Java基础

1、Java 语言有哪些特点?

  1. 简单易学;
  2. 面向对象(封装,继承,多态);
  3. 平台无关性( Java 虚拟机实现平台无关性);
  4. 支持多线程( C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持);
  5. 可靠性;
  6. 安全性;
  7. 支持网络编程并且很方便( Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便);
  8. 编译与解释并存;

2、JVM、JRE和JDK的关系

JVM
Java Virtual Machine是Java虚拟机,Java程序需要运行在虚拟机上,不同的平台有自己的虚拟机,因此Java语言可以实现跨平台。

JRE
Java Runtime Environment包括Java虚拟机和Java程序所需的核心类库等。核心类库主要是java.lang包:包含了运行Java程序必不可少的系统类,如基本数据类型、基本数学函数、字符串处理、线程、异常处理类等,系统缺省加载这个包

如果想要运行一个开发好的Java程序,计算机中只需要安装JRE即可。

JDK
Java Development Kit是提供给Java开发人员使用的,其中包含了Java的开发工具,也包括了JRE。所以安装了JDK,就无需再单独安装JRE了。其中的开发工具:编译工具(javac.exe),打包工具(jar.exe)等

  1. JDK 用于开发,JRE 用于运行java程序 ;如果只是运行Java程序,可以只安装JRE,无序安装JDK。 微信搜索公众号:Java专栏,获取最新面试手册
  2. JDk包含JRE,JDK 和 JRE 中都包含 JVM。
  3. JVM 是 Java 编程语言的核心并且具有平台独立性。

3、Oracle JDK 和 OpenJDK 的对比

  • Oracle JDK版本将每三年发布一次,而OpenJDK版本每三个月发布一次;
  • OpenJDK 是一个参考模型并且是完全开源的,而Oracle JDK是OpenJDK的一个实现,并不是完全 开源的;
  • Oracle JDK 比 OpenJDK 更稳定。OpenJDK和Oracle JDK的代码几乎相同,但Oracle JDK有更多的 类和一些错误修复。因此,如果您想开发企业/商业软件,我建议您选择Oracle JDK,因为它经过了 彻底的测试和稳定。某些情况下,有些人提到在使用OpenJDK 可能会遇到了许多应用程序崩溃的 问题,但是,只需切换到Oracle JDK就可以解决问题;
  • 在响应性和JVM性能方面,Oracle JDK与OpenJDK相比提供了更好的性能;
  • Oracle JDK不会为即将发布的版本提供长期支持,用户每次都必须通过更新到最新版本获得支持来 获取最新版本;
  • Oracle JDK根据二进制代码许可协议获得许可,而OpenJDK根据GPL v2许可获得许可。

拓展一下:

  • BCL 协议(Oracle Binary Code License Agreement): 可以使用 JDK(支持商用),但是不能进行修改。
  • OTN 协议(Oracle Technology Network License Agreement): 11 及之后新发布的 JDK 用的都是这个协议,可以自己私下用,但是商用需要付费。

4、Java有哪些数据类型

Java中有 8 种基本数据类型,分别为:

​ 6 种数字类型 (四个整数形,两个浮点型):byte、short、int、long、float、double

​ 1 种字符类型:char

​ 1 种布尔型:boolean。

引用数据类型

  • 类(class)
  • 接口(interface)
  • 数组([])

简单来说,只要不是基本数据类型.都是引用数据类型。 那他们有什么不同呢?

1、从概念方面来说

​ 1,基本数据类型:变量名指向具体的数值

​ 2,引用数据类型:变量名不是指向具体的数值,而是指向存数据的内存地址,.也及时hash值

2、从内存的构建方面来说(内存中,有堆内存和栈内存两者)

​ 1,基本数据类型:被创建时,在栈内存中会被划分出一定的内存,并将数值存储在该内存中.

​ 2,引用数据类型:被创建时,首先会在栈内存中分配一块空间,然后在堆内存中也会分配一块具体的空间用来 存储数据的具体信息,即hash值,然后由栈中引用指向堆中的对象地址.

5、== 和 equals() 的区别

== 对于基本类型和引用类型的作用效果是不同的:

  • 对于基本数据类型来说,== 比较的是值。
  • 对于引用数据类型来说,== 比较的是对象的内存地址。

因为 Java 只有值传递,所以,对于 == 来说,不管是比较基本数据类型,还是引用数据类型的变量,其本质比较的都是值,只是引用类型变量存的值是对象的地址。

equals() 不能用于判断基本数据类型的变量,只能用来判断两个对象是否相等。equals()方法存在于Object类中,而Object类是所有类的直接或间接父类。

6、访问修饰符 public,private,protected,以及不写(默认)时的区别

**定义:**Java中,可以使用访问修饰符来保护对类、变量、方法和构造方法的访问。Java 支持 4 种不同的访问权限。

分类

private : 在同一类内可见。使用对象:变量、方法。 注意:不能修饰类(外部类)
default (即缺省,什么也不写,不使用任何关键字): 在同一包内可见,不使用任何修饰符。使用对象:类、接口、变量、方法。
protected : 对同一包内的类和所有子类可见。使用对象:变量、方法。 注意:不能修饰类(外部类)。
public : 对所有类可见。使用对象:类、接口、变量、方法

修饰符 当前类 同包 子类 其他包
private × × ×
default × ×
protected ×
public

7、final 有什么用?

用于修饰类、属性和方法;

  • 被final修饰的类不可以被继承
  • 被final修饰的方法不可以被重写
  • 被final修饰的变量不可以被改变,被final修饰不可变的是变量的引用,而不是引用指向的内容,引用指向的内容是可以改变的

8、final finally finalize区别

  • final可以修饰类、变量、方法,修饰类表示该类不能被继承、修饰方法表示该方法不能被重写、修饰变量表示该变量是一个常量不能被重新赋值。
  • finally一般作用在try-catch代码块中,在处理异常的时候,通常我们将一定要执行的代码方法finally代码块中,表示不管是否出现异常,该代码块都会执行,一般用来存放一些关闭资源的代码。
  • finalize是一个方法,属于Object类的一个方法,而Object类是所有类的父类,该方法一般由垃圾回收器来调用,当我们调用System.gc() 方法的时候,由垃圾回收器调用finalize(),回收垃圾,一个对象是否可回收的最后判断。

9、this关键字的用法

this是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。

this的用法在java中大体可以分为3种:

1.普通的直接引用,this相当于是指向当前对象本身。

2.形参与成员名字重名,用this来区分

3.引用本类的构造函数

10、super关键字的用法

super可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。

super也有三种用法:

1.普通的直接引用

​ 与this类似,super相当于是指向当前对象的父类的引用,这样就可以用super.xxx来引用父类的成员。

2.子类中的成员变量或方法与父类中的成员变量或方法同名时,用super进行区分

3、引用父类构造函数

  • super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。
  • this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。

11、this与super的区别

  • super: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名 super.成员函数据名(实参)
  • this:它代表当前对象名(在程序中易产生二义性之处,应使用this来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用this来指明成员变量名)
  • super()和this()类似,区别是,super()在子类中调用父类的构造方法,this()在本类内调用本类的其它构造方法。
  • super()和this()均需放在构造方法内第一行。
  • 尽管可以用this调用一个构造器,但却不能调用两个。
  • this和super不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。
  • this()和super()都指的是对象,所以,均不可以在static环境中使用。包括:static变量,static方法,static语句块。
  • 从本质上讲,this是一个指向本对象的指针, 然而super是一个Java关键字。

12、continue、break 和 return 的区别是什么?

在循环结构中,当循环条件不满足或者循环次数达到要求时,循环会正常结束。但是,有时候可能需要在循环的过程中,当发生了某种条件之后 ,提前终止循环,这就需要用到下面几个关键词:

  1. continue :指跳出当前的这一次循环,继续下一次循环。
  2. break :指跳出整个循环体,继续执行循环下面的语句。

return 用于跳出所在方法,结束该方法的运行。return 一般有两种用法:

  1. return; :直接使用 return 结束方法执行,用于没有返回值函数的方法
  2. return value; :return 一个特定值,用于有返回值函数的方法

13、 Java中的自动装箱与拆箱

  • 装箱:将基本类型用它们对应的引用类型包装起来;
  • 拆箱:将包装类型转换为基本数据类型;

装箱其实就是调用了 包装类的valueOf()方法,拆箱其实就是调用了 xxxValue()方法。

因此,

  • Integer i = 10 等价于 Integer i = Integer.valueOf(10)
  • int n = i 等价于 int n = i.intValue();

注意:如果频繁拆装箱的话,也会严重影响系统的性能。我们应该尽量避免不必要的拆装箱操作。

14、为什么要有包装类型?

让基本数据类型也具有对象的特征,就出现了包装类型(如我们在使用集合类型Collection时就一定要使 用包装类型而非基本类型)因为容器都是装object的,这是就需要这些基本类型的包装器类了。

二者的区别:

  1. 声明方式不同:基本类型不使用new关键字,而包装类型需要使用new关键字来在堆中分配存储空 间;
  2. 存储方式及位置不同:基本类型是直接将变量值存储在栈中,而包装类型是将对象放在堆中,然后 通过引用来使用;
  3. 初始值不同:基本类型的初始值如int为0,boolean为false,而包装类型的初始值为null;
  4. 使用方式不同:基本类型直接赋值直接使用就好,而包装类型在集合如Collection、Map时会使用到。

15、Hashcode的作用

java的集合有两类,一类是List,还有一类是Set。前者有序可重复,后者无序不重复。当我们在set中插入的时候怎么判断是否已经存在该元素呢,可以通过equals方法。但是如果元素太多,用这样的方法就 会比较满。

于是有人发明了哈希算法来提高集合中查找元素的效率。 这种方式将集合分成若干个存储区域,每个对 象可以计算出一个哈希码,可以将哈希码分组,每组分别对应某个存储区域,根据一个对象的哈希码就 可以确定该对象应该存储的那个区域。

hashCode方法可以这样理解:它返回的就是根据对象的内存地址换算出的一个值。这样一来,当集合要添加新的元素时,先调用这个元素的hashCode方法,就一下子能定位到它应该放置的物理位置上。 如果这个位置上没有元素,它就可以直接存储在这个位置上,不用再进行任何比较了;如果这个位置上已经有元素了,就调用它的equals方法与新元素进行比较,相同的话就不存了,不相同就散列其它的地 址。这样一来实际调用equals方法的次数就大大降低了,几乎只需要一两次。

16、hashCode 与 equals

HashSet如何检查重复

两个对象的 hashCode() 相同,则 equals() 也一定为 true,对吗?

hashCode和equals方法的关系

面试官可能会问你:“你重写过 hashcode 和 equals 么,为什么重写equals时必须重写hashCode方法?”

hashCode()介绍

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。

散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

为什么要有 hashCode

我们以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode:

当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。(摘自我的Java启蒙书《Head first java》第二版)。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

hashCode()与equals()的相关规定

如果两个对象相等,则hashcode一定也是相同的

两个对象相等,对两个对象分别调用equals方法都返回true

两个对象有相同的hashcode值,它们也不一定是相等的

因此,equals 方法被覆盖过,则 hashCode 方法也必须被覆盖

hashCode() 的默认行为是对堆上的对象产生独特值。如果没有重写 hashCode(),则该 class 的两个对象无论如何都不会相等(即使这两个对象指向相同的数据)

对象的相等与指向他们的引用相等,两者有什么不同?

对象的相等 比的是内存中存放的内容是否相等而 引用相等 比较的是他们指向的内存地址是否相等。

17、为什么重写 equals() 时必须重写 hashCode() 方法?

因为两个相等的对象的 hashCode 值必须是相等。也就是说如果 equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。

如果重写 equals() 时没有重写 hashCode() 方法的话就可能会导致 equals 方法判断是相等的两个对象,hashCode 值却不相等。

思考 :重写 equals() 时没有重写 hashCode() 方法的话,使用 HashMap 可能会出现什么问题。

总结

  • equals 方法判断两个对象是相等的,那这两个对象的 hashCode 值也要相等。
  • 两个对象有相同的 hashCode 值,他们也不一定是相等的(哈希碰撞)。

18、面向对象和面向过程的区别

两者的主要区别在于解决问题的方式不同:

  • 面向过程把解决问题的过程拆成一个个方法,通过一个个方法的执行解决问题。
  • 面向对象会先抽象出对象,然后用对象执行方法的方式解决问题。

另外,面向对象开发的程序一般更易维护、易复用、易扩展。

19、成员变量与局部变量的区别有哪些?

  • 语法形式 :从语法形式上看,成员变量是属于类的,而局部变量是在代码块或方法中定义的变量或是方法的参数;成员变量可以被 public,private,static 等修饰符所修饰,而局部变量不能被访问控制修饰符及 static 所修饰;但是,成员变量和局部变量都能被 final 所修饰。
  • 存储方式 :从变量在内存中的存储方式来看,如果成员变量是使用 static 修饰的,那么这个成员变量是属于类的,如果没有使用 static 修饰,这个成员变量是属于实例的。而对象存在于堆内存,局部变量则存在于栈内存。
  • 生存时间 :从变量在内存中的生存时间上看,成员变量是对象的一部分,它随着对象的创建而存在,而局部变量随着方法的调用而自动消失。
  • 默认值 :从变量是否有默认值来看,成员变量如果没有被赋初,则会自动以类型的默认值而赋值(一种情况例外:被 final 修饰的成员变量也必须显式地赋值),而局部变量则不会自动赋值。

20、面向对象三大特征

封装

封装是指把一个对象的状态信息(也就是属性)隐藏在对象内部,不允许外部对象直接访问对象的内部信息。但是可以提供一些可以被外界访问的方法来操作属性。就好像我们看不到挂在墙上的空调的内部的零件信息(也就是属性),但是可以通过遥控器(方法)来控制空调。如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。就好像如果没有空调遥控器,那么我们就无法操控空凋制冷,空调本身就没有意义了(当然现在还有很多其他方法 ,这里只是为了举例子)。

继承

不同类型的对象,相互之间经常有一定数量的共同点。例如,小明同学、小红同学、小李同学,都共享学生的特性(班级、学号等)。同时,每一个对象还定义了额外的特性使得他们与众不同。例如小明的数学比较好,小红的性格惹人喜爱;小李的力气比较大。继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能,但不能选择性地继承父类。通过使用继承,可以快速地创建新的类,可以提高代码的重用,程序的可维护性,节省大量创建新类的时间 ,提高我们的开发效率。

关于继承如下 3 点请记住:

  1. 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有
  2. 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
  3. 子类可以用自己的方式实现父类的方法。(以后介绍)。

多态

多态,顾名思义,表示一个对象具有多种的状态,具体表现为父类的引用指向子类的实例。

多态的特点:

  • 对象类型和引用类型之间具有继承(类)/实现(接口)的关系;
  • 引用类型变量发出的方法调用的到底是哪个类中的方法,必须在程序运行期间才能确定;
  • 多态不能调用“只在子类存在但在父类不存在”的方法;
  • 如果子类重写了父类的方法,真正执行的是子类覆盖的方法,如果子类没有覆盖父类的方法,执行的是父类的方法。

21、什么是内部类?

在Java中,可以将一个类的定义放在另外一个类的定义内部,这就是内部类。内部类本身就是类的一个属性,与其他属性定义方式一致。

22、内部类的分类有哪些

内部类可以分为四种:成员内部类、局部内部类、匿名内部类和静态内部类

23、抽象的关键字是什么?

Abstract

24、抽象类必须要有抽象方法吗

不是必须。抽象类可以没有抽象方法。

25、如果一个类中有抽象方法,那么这个一定是抽象类?

包含抽象方法的类一定是抽象类

26、抽象类可以使用final修饰吗?

不可以。定义抽象类就是让其他继承的,而final修饰类表示该类不能被继承,与抽象类的理念违背了

27、普通类与抽象类有什么区别?

普通类不能包含抽象方法,抽象类可以包含抽象方法
抽象类不能直接实例化,普通类可以直接实例化

28、什么是接口?

接口就是某个事物对外提供的一些功能的声明,是一种特殊的java类

29、JAVA为什么需要接口?

接口弥补了java单继承的缺点

30、接口有什么特点?

接口中声明全是public static final修饰的常量
接口中所有方法都是抽象方法
接口是没有构造方法的
接口也不能直接实例化
接口可以多继承

31、接口与抽象类有什么区别?

抽象类有构造方法,接口没有构造方法
抽象类只能单继承,接口可以多继承
抽象类可以有普通方法,接口中的所有方法都是抽象方法
接口的属性都是public static final修饰的,而抽象的不是

32、Java中异常分为哪两种?

编译时异常
运行时异常

33、说几个常见的编译时异常类?

NullPointerException:空指针异常
ArrayIndexOutOfBoundsException:数组下标越界
NumberFormatException:数字转换异常
IllegalArgumentException:参数不匹配异常
InstantiationException:对象初始化异常
ArithmeticException:算术异常

34、异常的处理机制有几种?

异常捕捉:try…catch…finally,异常抛出:throws。

35、如何自定义一个异常

继承一个异常类,通常是RumtimeException或者Exception

36、在异常捕捉时,如果发生异常,那么try.catch.finally块外的return语句会执行吗?

会执行,如果有finally,在finally之后被执行,如果没有finally,在catch之后被执行

37、Try.catch.finally是必须要存在的吗?

Try块必须存在,catch和finally可以不存在,但不能同时不存在

38、Thow与thorws区别

Throw写在代码块内,throw后面跟的是一个具体的异常实例
Throw写在方法前面后面,throws后面跟的是异常类,异常类可以出现多个

39、Error与Exception区别?

Error和Exception都是java错误处理机制的一部分,都继承了Throwable类。

Exception表示的异常,异常可以通过程序来捕捉,或者优化程序来避免。

Error表示的是系统错误,不能通过程序来进行错误处理。

40、线程同步的方法

  1. wait():让线程等待。将线程存储到一个线程池中。
  2. notify():唤醒被等待的线程。通常都唤醒线程池中的第一个。让被唤醒的线程处于临时阻塞状态。
  3. notifyAll(): 唤醒所有的等待线程。将线程池中的所有线程都唤醒。

41、线程与进程的区别

进程是系统进行资源分配和调度的一个独立单位,线程是CPU调度和分派的基本单位

进程和线程的关系:

  1. 一个线程只能属于一个进程,而一个进程可以有多个线程,但至少有一个线程。
  2. 资源分配给进程,同一进程的所有线程共享该进程的所有资源。
  3. 线程在执行过程中,需要协作同步。不同进程的线程间要利用消息通信的办法实现同步。
  4. 线程是指进程内的一个执行单元,也是进程内的可调度实体。

线程与进程的区别:

  1. 调度:线程作为调度和分配的基本单位,进程作为拥有资源的基本单位。
  2. 并发性:不仅进程之间可以并发执行,同一个进程的多个线程之间也可以并发执行。
  3. 拥有资源:进程是拥有资源的一个独立单位,线程不拥有系统资源,但可以访问隶属于进程的资源。
  4. 系统开销:在创建或撤销进程的时候,由于系统都要为之分配和回收资源,导致系统的明显大于创建或撤销线程时的开销。但进程有独立的地址空间,进程崩溃后,在保护模式下不会对其他的进程产生影响,而线程只是一个进程中的不同的执行路径。线程有自己的堆栈和局部变量,但线程之间没有单独的地址空间,一个线程死掉就等于整个进程死掉,所以多进程的程序要比多线程的程序健壮,但是在进程切换时,耗费的资源较大,效率要差些。

43、&和&&的区别

&是位运算符。&&是布尔逻辑运算符,在进行逻辑判断时用&处理的前面为false后面的内容仍需处理,用&&处理的前面为false不再处理后面的内容。

44、重载与重写

  1. Overload为重载,Override为重写方法的重写和重载是Java多态性的不同表现。重写是父类与子类之间多态性的一种表现,重载是一个类中多态性的一种表现。
  2. 如果在子类中定义某方法与其父类有相同的名称和参数,我们说该方法被重写 (Override)。子类的对象使用这个方法时,将调用子类中的定义,对它而言,父类中的定义如同被"屏蔽"了。
  3. 如果在一个类中定义了多个同名的方法,它们或有不同的参数个数或有不同的参数类型,则称为方法的重载(Overload)。
    重载的方法是可以改变返回值的类型。

45、如果对象的引用被置为null,垃圾收集器是否会立即释放对象占用的内存?

不会,在下一个垃圾回收周期中,这个对象将是可被回收的。

46、串行(serial)收集器和吞吐量(throughput)收集器的区别是什么?

吞吐量收集器使用并行版本的新生代垃圾收集器,它用于中等规模和大规模数据的应用程序。而串行收集器对大多数的小应用(在现代处理器上需要大概100M左右的内存)就足够了。

47、Java 中操作字符串都有哪些类?它们之间有什么区别?

操作字符串的类有: String 、 StringBuffer 、 StringBuilder 。

String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成 新的 String 对象,然后将指针指向新的 String 对象。

而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String

StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是 非线程安全的,但 StringBuilder 的性能却高于 StringBuffer, 所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。

48、String、StringBuffer和StringBuilder区别(类似上一题)

1、数据可变和不可变

1.String 底层使用一个不可变的字符数组 private final char value[]; 所以它内容不可变。

2.StringBuffer 和 StringBuilder 都继承了 AbstractStringBuilder 底层使用的是可变字符数 组: char[] value;

2、线程安全

StringBuilder 是线程不安全的,效率较高;而 StringBuffer 是线程安全的,效率较低。 通过他们的 append() 方法来看, StringBuffer 是有同步锁,而 StringBuilder 没有:

3、 相同点

StringBuilder 与 StringBuffer 有公共父类 AbstractStringBuilder 。 最后,操作可变字符串速度: StringBuilder > StringBuffer > String ,这个答案就显得不足为奇 了。

49、反射

什么是反射机制?
JAVA反射机制是在运行状态中,对于任意一个类,都能够知道这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意一个方法和属性;这种动态获取的信息以及动态调用对象的方法的功能称为java语言的反射机制。

静态编译和动态编译

**静态编译:**在编译时确定类型,绑定对象
**动态编译:**运行时确定类型,绑定对象

反射机制优缺点
优点: 运行期类型的判断,动态加载类,提高代码灵活度。
缺点: 性能瓶颈:反射相当于一系列解释操作,通知 JVM 要做的事情,性能比直接的java代码要慢很多。

反射机制的应用场景有哪些?
反射是框架设计的灵魂。

在我们平时的项目开发过程中,基本上很少会直接使用到反射机制,但这不能说明反射机制没有用,实际上有很多设计、开发都与反射机制有关,例如模块化的开发,通过反射去调用对应的字节码;动态代理设计模式也采用了反射机制,还有我们日常使用的 Spring/Hibernate 等框架也大量使用到了反射机制。

举例:①我们在使用JDBC连接数据库时使用Class.forName()通过反射加载数据库的驱动程序;②Spring框架也用到很多反射机制,最经典的就是xml的配置模式。Spring 通过 XML 配置模式装载 Bean 的过程:1) 将程序内所有 XML 或 Properties 配置文件加载入内存中; 2)Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息; 3)使用反射机制,根据这个字符串获得某个类的Class实例; 4)动态配置实例的属性

Java获取反射的三种方法
1.通过new对象实现反射机制

2.通过路径实现反射机制

3.通过类名实现反射机制

50、注解

Annontation (注解) 是Java5 开始引入的新特性,可以看作是一种特殊的注释,主要用于修饰类、方法或者变量。

注解本质是一个继承了Annotation 的特殊接口

注解只有被解析之后才会生效,常见的解析方法有两种:

  • 编译期直接扫描 :编译器在编译 Java 代码的时候扫描对应的注解并处理,比如某个方法使用@Override 注解,编译器在编译的时候就会检测当前的方法是否重写了父类对应的方法。
  • 运行期通过反射处理 :像框架中自带的注解(比如 Spring 框架的 @Value@Component)都是通过反射来进行处理的。

JDK 提供了很多内置的注解(比如 @Override@Deprecated),同时,我们还可以自定义注解。

51、Java 中 IO 流分为几种?

  • 按照流的流向分,可以分为输入流和输出流;
  • 按照操作单元划分,可以划分为字节流和字符流;
  • 按照流的角色划分为节点流和处理流。

Java IO 流共涉及 40 多个类,这些类看上去很杂乱,但实际上很有规则,而且彼此之间存在非常紧密的联系, Java IO 流的 40 多个类都是从如下 4 个抽象类基类中派生出来的。

  • InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
  • OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。

52、BIO,NIO,AIO 有什么区别?

  • BIO:Block IO 同步阻塞式 IO,就是我们平常使用的传统 IO,它的特点是模式简单使用方便,并发处理能力低。
  • NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端通过 Channel(通道)通讯,实现了多路复用。
  • AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非堵塞 IO ,异步 IO 的操作基于事件和回调机制。

53、什么是集合

**集合框架:**用于存储数据的容器。

集合框架是为表示和操作集合而规定的一种统一的标准的体系结构。
任何集合框架都包含三大块内容:对外的接口、接口的实现和对集合运算的算法。

**接口:**表示集合的抽象数据类型。接口允许我们操作集合时不必关注具体实现,从而达到“多态”。在面向对象编程语言中,接口通常用来形成规范。

**实现:**集合接口的具体实现,是重用性很高的数据结构。

**算法:**在一个实现了某个集合框架中的接口的对象身上完成某种有用的计算的方法,例如查找、排序等。这些算法通常是多态的,因为相同的方法可以在同一个接口被多个类实现时有不同的表现。事实上,算法是可复用的函数。
它减少了程序设计的辛劳。

集合框架通过提供有用的数据结构和算法使你能集中注意力于你的程序的重要部分上,而不是为了让程序能正常运转而将注意力于低层设计上。
通过这些在无关API之间的简易的互用性,使你免除了为改编对象或转换代码以便联合这些API而去写大量的代码。 它提高了程序速度和质量

54、 简述Java中的集合

  1. Collection下:List系(有序、元素允许重复)和Set系(无序、元素不重复)

    set根据equals和hashcode判断,一个对象要存储在Set中,必须重写equals和hashCode方法

  2. Map下:HashMap线程不同步;TreeMap线程同步

  3. Collection系列和Map系列:Map是对Collection的补充,两个没什么关系

55、List、Map、Set三个接口,存取元素时,各有什么特点?

首先,List与Set具有相似性,它们都是单列元素的集合,所以,它们有一个共同的父接口,叫 Collection。

1、Set里面不允许有重复的元素

即不能有两个相等(注意,不是仅仅是相同)的对象,即假设Set集合中有了一个A对象,现在我要向Set 集合再存入一个B对象,但B对象与A对象equals相等,则B对象存储不进去,所以,Set集合的add方法 有一个boolean的返回值,当集合中没有某个元素,此时add方法可成功加入该元素时,则返回true, 当集合含有与某个元素equals相等的元素时,此时add方法无法加入该元素,返回结果为false。Set取 元素时,不能细说要取第几个,只能以Iterator接口取得所有的元素,再逐一遍历各个元素。

2、List表示有先后顺序的集合

注意,不是那种按年龄、按大小、按价格之类的排序。当我们多次调用add(Obje)方法时,每次加入的对 象就像火车站买票有排队顺序一样,按先来后到的顺序排序。有时候,也可以插队,即调用 add(intindex,Obj e)方法,就可以指定当前对象在集合中的存放位置。一个对象可以被反复存储进List 中,每调用一次add方法,这个对象就被插入进集合中一次,其实,并不是把这个对象本身存储进了集 合中,而是在集合中用一个索引变量指向这个对象,当这个对象被add多次时,即相当于集合中有多个 索引指向了这个对象。List除了可以用Iterator接口取得所有的元素,再逐一遍历各个元素之外,还可以 调用get(index i)来明确说明取第几个。

3、Map与List和Set不同

它是双列的集合,其中有put方法,定义如下:put(obj key,obj value),每次存储时,要存储一对 key/value,不能存储重复的key,这个重复的规则也是按equals比较相等。取则可以根据key获得相应 的value,即get(Object key)返回值为key所对应的value。另外,也可以获得所有的key的结合,还可以 获得所有的value的结合,还可以获得key和value组合成的Map.Entry对象的集合。

总结

List以特定次序来持有元素,可有重复元素。Set无法拥有重复元素,内部排序。Map保存key-value值, value可多值。

56、集合框架底层数据结构

List

  • ArraylistObject[] 数组
  • VectorObject[] 数组
  • LinkedList: 双向链表(JDK1.6 之前为循环链表,JDK1.7 取消了循环)

Set

  • HashSet(无序,唯一): 基于 HashMap 实现的,底层采用 HashMap 来保存元素
  • LinkedHashSet: LinkedHashSetHashSet 的子类,并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的 LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的
  • TreeSet(有序,唯一): 红黑树(自平衡的排序二叉树)

Map

  • HashMap: JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的(“拉链法”解决冲突)。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间
  • LinkedHashMapLinkedHashMap 继承自 HashMap,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:《LinkedHashMap 源码详细分析(JDK1.8)》
  • Hashtable: 数组+链表组成的,数组是 Hashtable 的主体,链表则是主要为了解决哈希冲突而存在的
  • TreeMap: 红黑树(自平衡的排序二叉树)

57、List 和 Set 的区别

List , Set 都是继承自Collection 接口

**List 特点:**一个有序(元素存入集合的顺序和取出的顺序一致)容器,元素可以重复,可以插入多个null元素,元素都有索引。常用的实现类有 ArrayList、LinkedList 和 Vector。

**Set 特点:**一个无序(存入和取出顺序有可能不一致)容器,不可以存储重复元素,只允许存入一个null元素,必须保证元素唯一性。Set 接口常用实现类是 HashSet、LinkedHashSet 以及 TreeSet。

另外 List 支持for循环,也就是通过下标来遍历,也可以用迭代器,但是set只能用迭代,因为他无序,无法用下标来取得想要的值。

Set和List对比

Set:检索元素效率低下,删除和插入效率高,插入和删除不会引起元素位置改变。
List:和数组类似,List可以动态增长,查找元素效率高,插入删除元素效率低,因为会引起其他元素位置改变

58、HashSet与HashMap的区别

HashMap HashSet
实现了Map接口 实现Set接口
存储键值对 仅存储对象
调用put()向map中添加元素 调用add()方法向Set中添加元素
HashMap使用键(Key)计算Hashcode HashSet使用成员对象来计算hashcode值,对于两个对象来说hashcode可能相同,所以equals()方法用来判断对象的相等性,如果两个对象不同的话,那么返回false
HashMap相对于HashSet较快,因为它是使用唯一的键获取对象 HashSet较HashMap来说比较慢

59、HashMap 和 Hashtable 的区别

  1. 线程是否安全: HashMap 是非线程安全的,Hashtable 是线程安全的,因为 Hashtable 内部的方法基本都经过synchronized 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
  2. 效率: 因为线程安全的问题,HashMap 要比 Hashtable 效率高一点。另外,Hashtable 基本被淘汰,不要在代码中使用它;
  3. 对 Null key 和 Null value 的支持: HashMap 可以存储 null 的 key 和 value,但 null 作为键只能有一个,null 作为值可以有多个;Hashtable 不允许有 null 键和 null 值,否则会抛出 NullPointerException
  4. 初始容量大小和每次扩充容量大小的不同 : ① 创建时如果不指定容量初始值,Hashtable 默认的初始大小为 11,之后每次扩充,容量变为原来的 2n+1。HashMap 默认的初始化大小为 16。之后每次扩充,容量变为原来的 2 倍。② 创建时如果给定了容量初始值,那么 Hashtable 会直接使用你给定的大小,而 HashMap 会将其扩充为 2 的幂次方大小(HashMap 中的tableSizeFor()方法保证,下面给出了源代码)。也就是说 HashMap 总是使用 2 的幂作为哈希表的大小,后面会介绍到为什么是 2 的幂次方。
  5. 底层数据结构: JDK1.8 以后的 HashMap 在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。Hashtable 没有这样的机制。

60、说一下 HashMap 的实现原理?

HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。此实现提供所有可选的映射操作,并允许使用null值和null键。此类不保证映射的顺序,特别是它不保证该顺序恒久不变。

HashMap的数据结构: 在Java编程语言中,最基本的结构就是两种,一个是数组,另外一个是模拟指针(引用),所有的数据结构都可以用这两个基本结构来构造的,HashMap也不例外。HashMap实际上是一个“链表散列”的数据结构,即数组和链表的结合体。

HashMap 基于 Hash 算法实现的

  1. 当我们往Hashmap中put元素时,利用key的hashCode重新hash计算出当前对象的元素在数组中的下标
  2. 存储时,如果出现hash值相同的key,此时有两种情况。(1)如果key相同,则覆盖原始值;(2)如果key不同(出现冲突),则将当前的key-value放入链表中
  3. 获取时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
  4. 理解了以上过程就不难明白HashMap是如何解决hash冲突的问题,核心就是使用了数组的存储方式,然后将冲突的key的对象放入链表中,一旦发现冲突就在链表中做进一步的对比。

需要注意Jdk 1.8中对HashMap的实现做了优化,当链表中的节点数据超过八个之后,该链表会转为红黑树来提高查询效率,从原来的O(n)到O(logn)

HashMap扩容机制

  • table:Node<K,V>类型的数组,里面的元素是链表,用于存放HashMap元素的实体

何时进行扩容

HashMap使用的是懒加载,构造完HashMap对象后,只要不进行put 方法插入元素之前,HashMap并不会去初始化或者扩容table

当首次调用put方法时,HashMap会发现table为空然后调用resize方法进行初始化

当添加完元素后,如果HashMap发现size(元素总数)大于threshold(阈值),则会调用resize方法进行扩容

resize扩容

当table需要扩容时,扩容后的table大小变为原来的两倍,接下来就是进行扩容后table的调整:

  1. 元素hash值第N+1位为0:不需要进行位置调整
  2. 元素hash值第N+1位为1:调整至原索引的两倍位置

在resize方法中,有一个判断用于确定元素hash值第N+1位是否为0:

  • 若为0,则使用loHead与loTail,将元素移至新table的原索引处

  • 若不为0,则使用hiHead与hiHead,将元素移至新table的两倍索引处

扩容或初始化完成后,resize方法返回新的table

61、如何决定使用 HashMap 还是 TreeMap?

对于在Map中插入、删除和定位元素这类操作,HashMap是最好的选择。然而,假如你需要对一个有序的key集合进行遍历,TreeMap是更好的选择。基于你的collection的大小,也许向HashMap中添加元素会更快,将map换为TreeMap进行有序key的遍历。

62、Collection 和 Collections 有什么区别?

  • java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。Collection接口在Java 类库中有很多具体的实现。Collection接口的意义是为各种具体的集合提供了最大化的统一操作方式,其直接继承接口有List与Set
  • Collections则是集合类的一个工具类/帮助类,其中提供了一系列静态方法,用于对集合中元素进行排序、搜索以及线程安全等各种操作

63、TreeMap 和 TreeSet 在排序时如何比较元素?Collections 工具类中的 sort()方法如何比较元素?

TreeSet 要求存放的对象所属的类必须实现 Comparable 接口,该接口提供了比较元素的 compareTo()方法,当插入元素时会回调该方法比较元素的大小。TreeMap 要求存放的键值对映射的键必须实现 Comparable 接口从而根据键对元素进行排序。

Collections 工具类的 sort 方法有两种重载的形式,

第一种要求传入的待排序容器中存放的对象比较实现 Comparable 接口以实现元素的比较;

第二种不强制性的要求容器中的元素必须可比较,但是要求传入第二个参数,参数是Comparator 接口的子类型(需要重写 compare 方法实现元素的比较),相当于一个临时定义的排序规则,其实就是通过接口注入比较元素大小的算法,也是对回调模式的应用(Java 中对函数式编程的支持)。

64、ArrayList与LinkedList有什么区别?

ArrayList与LinkedList都实现了List接口。
ArrayList是线性表,底层是使用数组实现的,它在尾端插入和访问数据时效率较高,
Linked是双向链表,他在中间插入或者头部插入时效率较高,在访问数据时效率较低

65、Array 和 ArrayList 有何区别?

  • Array 可以存储基本数据类型和对象,ArrayList 只能存储对象。
  • Array 是指定固定大小的,而 ArrayList 大小是自动扩展的。
  • Array 内置方法没有 ArrayList 多,比如 addAll、removeAll、iteration 等方法只有 ArrayList 有。

对于基本类型数据,集合使用自动装箱来减少编码工作量。但是,当处理固定大小的基本数据类型的时候,这种方式相对比较慢。

66、如何实现 Array 和 List 之间的转换?

  • Array 转 List: Arrays. asList(array) ;
  • List 转 Array:List 的 toArray() 方法。

Java并发

67、什么是线程和进程?

进程

一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在Windows系统中,一个运行的xx.exe就是一个进程。

线程

进程中的一个执行任务(控制单元),负责当前进程中程序的执行。一个进程至少有一个线程,一个进程可以运行多个线程,多个线程可共享数据。

68、进程与线程的区别

  1. 一个进程包含一个或多个线程

  2. 每个进程都有自己独立的内存空间,线程没有自己独立的内存空间,线程共享所在进程的内存空间

  3. 进程是重量级的单元,需要系统资源比较多,线程是轻量级单元,需要资源比较少

69、多线程和单线程的区别和联系?

1、在单核 CPU 中,将 CPU 分为很小的时间片,在每一时刻只能有一个线程在执行,是一种微观上轮流
占用 CPU 的机制。
2、多线程会存在线程上下文切换,会导致程序执行速度变慢,即采用一个拥有两个线程的进程执行所需
要的时间比一个线程的进程执行两次所需要的时间要多一些。
结论:即采用多线程不会提高程序的执行速度,反而会降低速度,但是对于用户来说,可以减少用户的
响应时间。

70、线程的创建方式

方法一:继承Thread类,作为线程对象存在(继承Thread对象)

方法二:实现runnable接口,作为线程任务存在

方法三:匿名内部类创建线程对象

方法四:创建带返回值的线程

方法五:定时器Timer

方法六:线程池创建线程

方法七:利用java8新特性 stream 实现并发

71、线程的生命周期

  1. 新建
    线程new出来,没有调用start方法,CPU没有分配时间片
  2. 就绪
    线程调用了start方法,CPU准备分配时间片,但是没有真正分配
  3. 运行
    线程抢到了CPU,开始执行run方法
  4. 阻塞

    进入阻塞状态:

    • 调用sleep方法
    • 线程的锁调用wait方法
    • 调用suspend(挂起)方法
    • 流的IO操作

      从阻塞恢复:

    • sleep的时间完毕
    • 锁调用notify或notifyAll方法
    • 调用resume(恢复)方法
    • IO操作完毕

阻塞状态结束后,回到就绪状态

  1. 死亡,run方法执行完毕后线程进入死亡状态

72、start()方法和run()方法的区别

只有调用了start()方法,才会表现出多线程的特性,不同线程的run()方法里面的代码交替执行。

如果只是调用run()方法,那么代码还是同步执行的,必须等待一个线程的run()方法里面的代码全部执行

完毕之后,另外一个线程才可以执行其run()方法里面的代码。

73、sleep方法和wait方法有什么区别和共同点

两者最主要的区别在于:sleep() 方法没有释放锁,而 wait() 方法释放了锁 。
两者都可以暂停线程的执行。
wait() 通常被用于线程间交互/通信,sleep() 通常被用于暂停执行。
wait() 方法被调用后,线程不会自动苏醒,需要别的线程调用同一个对象上的 notify() 或者 notifyAll() 方法。sleep() 方法执行完成后,线程会自动苏醒。或者可以使用 wait(long timeout) 超时后线程会自动苏醒。

74、如何确保线程安全?

  • 对非安全的代码进行加锁控制
  • 使用线程安全的类
  • 多线程并发情况下,线程共享的变量改为方法级的局部变量

75、 同步方法和同步块区别,以及哪个是更好的选择?

  • 同步块可以控制代码的范围,同步方法是整个方法上锁
  • 同步块可以将任意的成员变量作为锁,同步方法只能以this作为锁
  • 同步块的性能高于同步方法

76、ThreadLocal是什么

从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该
变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可
以访问自己内部的副本变量。
从字面意思来看非常容易理解,但是从实际使用的角度来看,就没那么容易了,作为一个面试常问的
点,使用场景那也是相当的丰富:
1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。

77、乐观锁和悲观锁的区别

  • 悲观锁
    想法悲观,认为当前的资源存在竞争,所以每次获得资源时都会上锁,阻塞住其它线程。
    数据库中的行锁、表锁、读锁、写锁都属于悲观锁,Java的synchronized和ReentrantLock也属于悲观锁。
    悲观锁会降低系统性能和吞吐量,提高数据的安全性,适用于多写少读的场景。
  • 乐观锁
    想法乐观,认为当前的资源不存在竞争,所以每次获得资源时都不上锁
    乐观锁执行效率高,有利于提高系统吞吐量,适用于多读少写的场景。

78、乐观锁如何实现

乐观锁常用的两种实现方式:

  1. 版本号机制
    利用版本号version记录数据被修改的次数,当数据被修改时,version加一。当线程要更新数据值时,在读取数据的同时也会读取version值,在提交更新时,若刚才读取到的version值为当前version值相等时才更新,否则重试更新操作,直到更新成功。
  2. CAS算法
    Compare and Swap(比较与交换)
    CAS涉及三个操作数:

    1. 读写变量的内存位置
    2. 预期值
    3. 写入的新值
      CAS实现过程是:先判断内存位置上的值是否和预期值一致,如果一致就修改为新值,否则不执行。

79、线程的等待和通知

​ 一旦线程进入同步块或同步方法,JVM会启动监视器监控线程的状态,线程都会持有锁,同步块持有是锁对象,同步方法的锁是this.

Object类的方法:

  • void wait() 让持有锁的线程进入等待状态,直到被通知
  • void wait(long time) 让线程等待,直到被通知或时间结束
  • void notify() 随机选择一个等待的线程,进行通知
  • void notifyAll() 通知所有等待的线程

注意:上面的方法只能是锁对象调用,否则出现异常IllegalMonitorStateException

80、wait()方法和notify()/notifyAll()方法在放弃对象监视器时有什么区别

​ wait()方法和notify()/notifyAll()方法在放弃对象监视器的时候的区别在于:wait()方法立即释放对象监视
器,notify()/notifyAll()方法则会等待线程剩余代码执行完毕才会放弃对象监视器。

81、什么是阻塞(Blocking)和非阻塞(Non-Blocking)?

​ 阻塞和非阻塞通常用来形容多线程间的相互影响。比如一个线程占用了临界区资源,那么其他所有需要
这个而资源的线程就必须在这个临界区中进行等待。等待会导致线程挂起,这种情况就是阻塞。此时,
如果占用资源的线程一直不愿意释放资源,那么其他所有阻塞在这个临界区上的线程都不能工作。
非阻塞的意思与之相反,它强调没有一个线程可以妨碍其他线程执行。所有的线程都会尝试不断前向执
行。

82、阻塞队列主要API

主要API:

  • BlockingQueue接口

    • void put(T t) 添加数据到末尾,队列满了会自动阻塞线程
    • T take() 从队列的头部取出数据并删除,队列空了会阻塞线程

BlockingQueue的主要实现类

  • ArrayBlockingQueue 数组阻塞队列
  • LinkedBlockingQueue 链表阻塞队列

83、 什么是线程池? 它的作用?

创建线程要花费昂贵的资源和时间,如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。为了避免这些问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。

线程池的作用是:首先会在池中分配一定数量的线程,线程使用完后会回到池中,等待下一个任务,线程资源就得到回收利用,减少服务器资源的消耗,提高了性能。

84、线程池的API

  • Executor接口
  • ExecutorService接口
  • AbstractExecutorService抽象类
  • ThreadPoolExecutor线程池类
  • Executors工具类

85、自定义线程池的配置

ThreadPoolExecutor的构造方法:

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue)

参数说明:

  1. corePoolSize 核心线程数
  2. maximumPoolSize 最大线程数
  3. keepAliveTime 临时的线程能够存活的时间
  4. unit 时间单位
  5. workQueue 用于保存任务的阻塞队列

优化配置:

  1. 核心线程数大于或等于CPU内核的数量(如果是CPU密集型任务,就需要尽量压榨CPU,参考值可以设为 NCPU+1,如果是IO密集型任务,参考值可以设置为2*NCPU)获得CPU的内核数:Runtime.getRuntime().availableProcessors()
  2. 最大线程数和核心线程数配置一样,性能比较高,因为避免临时线程的创建和销毁
  3. 如果临时线程比较多,可以将存活时间配置稍微长点,减少临时线程的创建和销毁
  4. 阻塞队列LinkedBlockingQueue的性能比较高

86、如何预防和避免线程死锁

如何预防死锁? 破坏死锁的产生的必要条件即可:

  1. 破坏请求与保持条件 :一次性申请所有的资源。
  2. 破坏不剥夺条件 :占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  3. 破坏循环等待条件 :靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

如何避免死锁?

避免死锁就是在资源分配时,借助于算法(比如银行家算法)对资源分配进行计算评估,使其进入安全状态。

安全状态 指的是系统能够按照某种进程推进顺序(P1、P2、P3…Pn)来为每个进程分配所需资源,直到满足每个进程对资源的最大需求,使每个进程都可顺利完成。称<P1、P2、P3…Pn>序列为安全序列。

87、什么是线程安全

又是一个理论的问题,各式各样的答案有很多,我给出一个个人认为解释地最好的:如果你的代码在多 线程下执行和在单线程下执行永远都能获得一样的结果,那么你的代码就是线程安全的。

1、不可变

像String、Integer、Long这些,都是final类型的类,任何一个线程都改变不了它们的值,要改变除非新 创建一个,因此这些不可变对象不需要任何同步手段就可以直接在多线程环境下使用

2、绝对线程安全

不管运行时环境如何,调用者都不需要额外的同步措施。要做到这一点通常需要付出许多额外的代价, Java中标注自己是线程安全的类,实际上绝大多数都不是线程安全的,不过绝对线程安全的类,Java中 也有,比方说CopyOnWriteArrayList、CopyOnWriteArraySet

3、相对线程安全

相对线程安全也就是我们通常意义上所说的线程安全,像Vector这种,add、remove方法都是原子操 作,不会被打断,但也仅限于此,如果有个线程在遍历某个Vector、有个线程同时在add这个Vector, 99%的情况下都会出现ConcurrentModificationException,也就是fail-fast机制。

4、线程非安全

这个就没什么好说的了,ArrayList、LinkedList、HashMap等都是线程非安全的类

88、锁机制

1、同步代码块

synchronized(锁对象){需要同步执行的代码
}

注意:任何对象都可以作为锁,必须是成员变量

原理:一旦线程进入了该代码块,就持有锁,JVM会有监视器监视进入锁的线程,其它线程想进入代码,监视器会拒绝访问;一旦持有锁的线程执行代码完毕,锁就被释放,其它线程就可以进入。

2、同步方法

public synchronized 返回值 方法名(参数..){方法的代码
}

同步块和同步方法的区别:

  1. 同步块可以控制代码的范围,同步方法是整个方法上锁
  2. 同步块可以将任意的成员变量作为锁,同步方法只能以this作为锁
  3. 同步块的性能高于同步方法

3、同步锁

Java1.5后出现的Lock包括:

  • ReentrantLock 重入锁,控制线程进入
  • ReadLock 读锁,控制线程读取
  • WriteLock 写锁,控制线程写入
  • ReadWriteLock 读写锁,控制线程读写

三种上锁机制的总结

  1. 同步块和同步方法出现早,同步锁在1.5后出现
  2. 性能:同步锁 > 同步块 > 同步方法
  3. 同步锁提供了大量的方法,也可以if配合使用,更加灵活

JVM面试题

89、什么是Java虚拟机?为何被称作“平台无关的编程语言”?

Java虚拟机是一个可以执行Java字节码(.class)的虚拟机进程。

Java源文件(.java)被编译成Java字节码(.class)。允许应用程序可以在任意平台。

因为Java虚拟机知道底层硬件平台的指令长度和其他特性。

90、Java的内存结构

(1)Java堆:虚拟机启动时创建,存放对象实例(new)。各线程共享。

(2)方法区:存储加载的类的信息、常量、静态变量、编译后的代码等。各线程共享。

(3)程序计数器:当前线程所执行的字节码的行号指示器。线程私有。

(4)JVM栈:描述Java方法执行的内存模型,每个栈帧对应一个方法,线程私有。

(5)本地方法栈:为虚拟机使用到的Native方法服务。

91、内存分配

(1)寄存器:无法控制。

(2)静态域:static定义的成员。

(3)常量池:编译时被确定并保存到.class文件中的(final)常量值和一些文本修饰的符号引用。

(4)非RAM存储:硬盘等永久存储空间。

(5)堆内存:new创建的对象和数组,由Java虚拟机自动垃圾回收器管理,存取速度慢。

栈内存:基本类型的变量和对象的引用变量(对内存空间的访问地址),速度快,可以共享,但是大小与生存周期必须确定,缺乏灵活性。

92、类加载器

(1)启动类加载器:负责加载JDK\jre\lib下的文件。

(2)扩展类加载器:负责加载JDK\jre\lib\ext目录中的所有类库。

(3)应用程序类加载器:负责加载用户类路径所指定的类。

93、Java对象是如何创建的

基本类型,直接在栈中创建并赋值

对象类型,在栈中创建对象的引用,在堆中划分内存存放对象实例数据,并与早在类加载时就加载到元空间中的实例或类成员方法、加载到常量池中的类的静态成员变量、发生引用连接。

成员变量、局部变量、类变量分别存储在内存的什么地方?

类变量是用static修饰符修饰,定义在方法外的变量,随着java进程产生和销毁在java8之前把静态变量存放于方法区,在java8时存放在堆中

成员变量是定义在类中,但是没有static修饰符修饰的变量,随着类的实例产生和销毁,是类实例的一部分由于是实例的一部分,在类初始化的时候,从运行时常量池取出直接引用或者值,与初始化的对象一起放入堆中

局部变量局部变量是定义在类的方法中的变量在所在方法被调用时放入虚拟机栈的栈帧中,方法执行结束后从虚拟机栈中弹出,所以存放在虚拟机栈中

94、什么情况下会发生栈溢出?

栈的大小可以通过-Xss参数设置,当递归层次太深的时候就会发生栈溢出。(例循环、递归)。

95、Java垃圾回收机制,GC是什么?为什么要GC?

Java垃圾回收机制:低优先级,只有在JVM空闲和当前堆内存不足时,会执行。扫描出那些没有被引用过的类,将他们添加到要回收的集合中,进行回收。

GC 是垃圾收集的意思。

Java 提供的 GC 功能可以自动监测对象是否超过作用域从而达到自动回收内存的目的。

96、JVM新生代、老年代、持久代都存储些哪些东西?

(1)新生代:new出的对象。

(2)老年代:新生代中经历了N次垃圾回收仍存活的对象、大对象直接存入、Survivor空间不足。

(3)持久代:指方法区。

97、Java会存在内存泄漏吗?

内存泄漏:指一个不再被程序使用的对象或变量一直被占据在内存中。

情况:长生命周期的对象只有断声明周期对象的引用时,可能发生内存泄漏。对生命周期对象已经不需要,但是因为长生命周期对象对其的引用,而无法回收。

98、JVM调优有哪些?

(1)设定堆内存大小。-Mmx:堆内存最大限制。

(2)设定新生代大小:(不能太小,否则会大量涌入老年代)

​ -XX:NewSize:新生代大小

​ -XX:NewRatio:新生代和老生代占比

​ -XX:SurvivorRatio:伊甸园空间和幸存者空间占比

(3)设定垃圾回收器:年轻代:-XX:+UseParNewGC

​ 老年代:-XX:+UseConcMarkSweepGC

99、虚拟机栈和本地方法栈溢出?

(1)线程请求的栈深度大于虚拟机所允许的最大深度。

(2)虚拟机扩展栈时无法申请到足够的内存空间。

100、方法区溢出

方法区存放Class相关信息,如类名、访问修饰符、常量池、字段描述、方法描述等。

保存的Class对象没有被及时回收掉,或者Class信息占用的内存超过了我们的配置。

101、说一下堆栈的区别?

物理地址 栈使用的是数据结构中的栈,先进后出的原则,物理地址分配是连续的。所以性能快。 堆的物理地址分配对对象是不连续的。因此性能慢些。
内存分别 栈是连续的,所以分配的内存大小要在编译期就确认,大小是固定的。 堆因为是不连续的,所以分配的内存是在运行期确认的,因此大小不固定。一般堆大小远远大于栈。
存放的内容 栈存放:局部变量,操作数栈,返回结果。该区更关注的是程序方法的执行。 堆存放的是对象的实例和数组。因此该区更关注的是数据的存储
PS 静态变量放在方法区 静态的对象还是放在堆。
程序的可见度 栈只对于线程是可见的。所以也是线程私有。他的生命周期和线程相同。 堆对于整个应用程序都是共享、可见的。

102、队列和栈是什么?有什么区别?

队列和栈都是被用来预存储数据的。

队列
操作的名称 队列的插入称为入队,队列的删除称为出队。 栈的插入称为进栈,栈的删除称为出栈。
可操作的方式 队列是在队尾入队,队头出队,即两边都可操作。 而栈的进栈和出栈都是在栈顶进行的,无法对栈底直接进行操作。
操作的方法 队列是先进先出(FIFO),即队列的修改是依先进先出的原则进行的。新来的成员总是加入队尾(不能从中间插入),每次离开的成员总是队列头上(不允许中途离队)。 而栈为后进先出(LIFO),即每次删除(出栈)的总是当前栈中最新的元素,即最后插入(进栈)的元素,而最先插入的被放在栈的底部,要到最后才能删除。

103、常用的 JVM 调优的参数都有哪些?

XX比X的稳定性更差,并且版本更新不会进行通知和说明。

  • -Xms

s为strating,表示堆内存起始大小

  • -Xmx

x为max,表示最大的堆内存

(一般来说-Xms和-Xmx的设置为相同大小,因为当heap自动扩容时,会发生内存抖动,影响程序的稳定性)

  • -Xmn

n为new,表示新生代大小

(-Xss:规定了每个线程虚拟机栈(堆栈)的大小)

  • -XX:SurvivorRator=8

​ 表示堆内存中新生代、老年代和永久代的比为8:1:1

  • -XX:PretenureSizeThreshold=3145728

​ 表示当创建(new)的对象大于3M的时候直接进入老年代

  • -XX:MaxTenuringThreshold=15

​ 表示当对象的存活的年龄(minor gc一次加1)大于多少时,进入老年代

  • -XX:-DisableExplicirGC

​ 表示是否(+表示是,-表示否)打开GC日志

104、 说一下 JVM 有哪些垃圾回收算法?

· **标记-清除算法:**标记无用对象,然后进行清除回收。缺点:效率不高,无法清除垃圾碎片。

· **标记-整理算法:**标记无用对象,让所有存活的对象都向一端移动,然后直接清除掉端边界以外的内存。

· **复制算法:**按照容量划分二个大小相等的内存区域,当一块用完的时候将活着的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。缺点:内存使用率不高,只有原来的一半。

· **分代算法:**根据对象存活周期的不同将内存划分为几块,一般是新生代和老年代,新生代基本采用复制算法,老年代采用标记整理算法。

105、什么是双亲委派模型?

在介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在 JVM 中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将 class 文件加载到 JVM 内存,然后再转化为 class 对象。

类加载器分类:

· 启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载Java_HOME/lib/目录中的,或者被 -Xbootclasspath 参数所指定的路径中并且被虚拟机识别的类库;

· 其他类加载器:

· 扩展类加载器(Extension ClassLoader):负责加载\lib\ext目录或Java. ext. dirs系统变量指定的路径中的所有类库;

· 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

双亲委派模型:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。

双亲委托机制

除了顶层的启动类加载器之外,其他加载器,在加载之前都会委派给它的父加载器进行加载。

一层一层向上传递,直到最顶层之后,它才会真正加载。

106、类加载过程

类加载机制主要就是加载、验证、准备、解析、初始化这些过程

加载(Loading)的过程中做了哪些处理

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将获取到的二进制字节流转化成一种数据结构并放进方法区
  • 在内存中生成一个代表此类的java.lang.Class对象,作为访问方法区中各种数据的接口

被加载的类的信息存储在方法区中,可以被线程所共享,也就是说,加载阶段完成后,虚拟机外部的二进制字节流就按照虚拟机所需的格式存储在了方法区之中。 Class对象虽然是在内存中,但并未明确规定是在Java堆中,对于HotSpot来说,Class对象存储在方法区中。它作为程序访问方法区中二进制字节流中所存储各种数据的接口。

验证阶段

这一阶段的目的主要是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,从而不会危害虚拟机自身安全。也就是说,当加载阶段将字节流加载进方法区之后,JVM需要做的第一件事就是对字节流进行安全校验,以保证格式正确,使自己之后能正确的解析到数据并保证这些数据不会对自身造成危害。

验证阶段主要分成四个子阶段:

  • 文件格式验证
  • 元数据验证
  • 字节码验证
  • 符号引用验证

准备阶段

1.准备阶段的目的:正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存将在方法区中分配。

注意我的重点:是类变量(static)不是实例变量,还有,我们又知道了在JVM的方法区中不仅存储着Class字节流(按照运行时方法区的数据结构进行存储,上述的二进制字节流是不严谨的说法,只是为了大家好理解),还有我们的类变量。

2.这里的类变量初始值通常是指数据类型的零值。比如int的零值为0,long为0L,boolean为false… …真正的初始化赋值是在初始化阶段进行的。

解析阶段

解析阶段的目的:虚拟机将常量池内的符号引用替换为直接引用

常量池(constant pool)指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。它包括了关于类、方法、接口等中的常量,也包括字符串常量。

符号引用:总的来说就是常量池中存储的那些描述类、方法、接口的字面量,你可以简单的理解为就是那些所需要信息的全限定名,目的就是为了虚拟机在使用的时候可以定位到所需要的目标。

直接引用:直接指向目标的指针、相对偏移量或能间接定位到目标的句柄。

重新解读:虚拟机将运行时常量池中那些仅代表其他信息的符号引用解析为直接指向所需信息所在地址的指针。

  • 在解析阶段主要有以下不同的动作

    • 类或接口的解析(注意数组类和非数组类)
    • 字段(简单名称+字段描述符)解析(注意递归搜索)
    • 类方法解析(注意递归搜索)
    • 接口方法解析(注意递归搜索)

动态连接

大部分JVM的实现都是延迟加载或者叫做动态连接。它的意思就是JVM装载某个类A时,如果类A中有引用其他类B,虚拟机并不会将这个类B也同时装载进JVM内存,而是等到执行的时候才去装载。

而这个被引用的B类在引用它的类A中的表现形式主要被登记在了符号表中,而解析的过程就是当需要用到被引用类B的时候,将引用类B在引用类A的符号引用名改为内存里的直接引用。

初始化阶段

初始化过程,将按照类文件中初始化代码,依次执行。

虚拟机规范定义了5种情况,会触发类的初始化阶段,也正是这个阶段,JVM才真正开始执行类中定义的Java程序代码:

  • new一个对象、读取一个类静态字段、调用一个类的静态方法的时候
  • 对类进行反射调用的时候
  • 初始化一个类,发现父类还没有初始化,则先初始化父类
  • main方法开始执行时所在的类
  • 用动态语言支持时,如果一个java.lang.invoke.MethodHandle实例后解析结果REF_putStatic,REF_getStatic,REF_invokeStatic的方法句柄时,当该方法句柄对应的类没有初始化时,需要初始化该类 。

另外有三种引用类的方式不会触发初始化(也就是类的加载),为以下三种:

  • 通过子类(类名)引用父类的静态字段,不会导致子类初始化(会引发父类的初始化、不会引发子类的初始化)
  • 通过数组定义来引用类,不会触发此类的初始化
  • 引用另一个类中的常量不会触发另一个类的初始化,原因在于“常量传播优化

数据结构

107、JAVA中常见的数据结构

  • 数组
  • 栈(Stack)
  • 队列(Queue)
  • 链表(LinkedList)
  • 树(Tree)
  • 哈希表(Hash)

108、数组

数组是内存中连续保存的多个相同类型的元素的数据结构。通过下标索引访问数组中的元素,索引从0开始。根据维度可以分为1维数组、2维数组……N维数组。

  • 优点

    通过下标直接定位元素位置,查找效率高。

  • 缺点

    • 长度固定
    • 数据类型都必须相同
    • 插入跟删除元素时效率比较低。

适合场景:大量查询,很少删除和插入。

109、栈

栈结构只能在一端操作,该操作端叫做栈顶,另一端叫做栈底。栈结构按照“后进先出”(Last In First Out, LIFO)的方式处理结点数据。

Java中提供了对于栈结构的实现,路径为java.util.Stack,它继承了Vector类。常用API如下:

  • public boolean empty(): 测试堆栈是否为空。

  • public E peek(): 查看堆栈顶部的对象,但不从堆栈中移除它。

  • public E pop(): 除堆栈顶部的对象,并作为此函数的值返回该对象。

  • public E push(E item): 把项压入堆栈顶部。

110、队列

队列是一种特殊的线性表,特殊之处在于它只允许在表的前端(front)进行删除操作,而在表的后端(rear)进行插入操作,和栈一样,队列是一种操作受限制的线性表。进行插入操作的端称为队尾,进行删除操作的端称为队头。

​ Java中提供了对于队列结构的接口,路径为java.util.QueueLinkedList是它的一个实现类。常用API如下:

  • boolean add(E e): 将指定的元素插入此队列。
  • E element(): 获取,但是不移除此队列的头。
  • boolean offer(E e): 将指定的元素插入此队列。
  • E peek(): 获取但不移除此队列的头。
  • E poll(): 获取并移除此队列的头,如果此队列为空,则返回 null。
  • E remove():获取并移除此队列的头。

111、链表

链表是一种物理存储单元上非连续、非顺序的存储结构,数据元素的逻辑顺序是通过链表中的指针连接次序实现的。每一个链表都包含多个节点,节点又包含两个部分,一个是数据域(储存节点含有的信息),一个是引用域(储存下一个节点或者上一个节点的地址)。

链表的特点

  • 获取数据麻烦,需要遍历查找,比数组慢
  • 插入、删除的效率比较高

链表从结构上分为单向链表双向链表

112、树

树(Tree)是n(n≥0)个结点的有限集T,并且当n>0时满足下列条件:
​ (1)有且仅有一个特定的称为根(Root)的结点;
​ (2)当n>1时,其余结点可以划分为m(m>0)个互不相交的有限集T1、T2 、…、Tm,每个集Ti(1≤i≤m)均为树,且称为树T的子树(SubTree)。
​ 特别地,不含任何结点(即n=0)的树,称为空树。

如下就是一棵树的结构:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-quUrVOQu-1655426298728)(E:\就业冲刺资料\Java第五阶段_day06_数据结构\Java第五阶段_day06_数据结构\assets\1610941255206.png)]

基本术语

  • **结点:**存储数据元素和指向子树的链接,由数据元素和构造数据元素之间关系的引用组成。
  • **孩子结点:**树中一个结点的子树的根结点称为这个结点的孩子结点,如图1中的A的孩子结点有B、C、D
    **双亲结点:**树中某个结点有孩子结点(即该结点的度不为0),该结点称为它孩子结点的双亲结点,也叫前驱结点。双亲结点和孩子结点是相互的,如图1中,A的孩子结点是B、C、D,B、C、D的双亲结点是A。
    **兄弟结点:**具有相同双亲结点(即同一个前驱)的结点称为兄弟结点,如图1中B、B、D为兄弟结点。
    **结点的度:**结点所有子树的个数称为该结点的度,如图1,A的度为3,B的度为2.
    **树的度:**树中所有结点的度的最大值称为树的度,如图1的度为3.
    **叶子结点:**度为0的结点称为叶子结点,也叫终端结点。如图1的K、L、F、G、M、I、J
    **分支结点:**度不为0的结点称为分支结点,也叫非终端结点。如图1的A、B、C、D、E、H
    **结点的层次:**从根结点到树中某结点所经路径的分支数称为该结点的层次。根结点的层次一般为1(也可以自己定义为0),这样,其它结点的层次是其双亲结点的层次加1.
    **树的深度:**树中所有结点的层次的最大值称为该树的深度(也就是最下面那个结点的层次)。

二叉树

二叉树(Binary Tree)是有限个节点的集合,这个集合可以是空集,也可以是一个根节点和颗不相交的子二叉树组成的集合,其中一颗树叫根的左子树,另一颗树叫右子树。所以二叉树是一个递归地概念。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QmR3ROAf-1655426298729)(E:\就业冲刺资料\Java第五阶段_day06_数据结构\Java第五阶段_day06_数据结构\assets\1610952183532.png)]

满二叉树

​ 一棵满二叉树就是高度为k,且拥有(2^k)-1个节点的二叉树,一棵满二叉树每个节点,要么都有两棵子树,要么都没有子树;而且每一层所有的节点之间必须要么都有两棵子树,要么都没子树。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-h5Znv6wv-1655426298729)(E:\就业冲刺资料\Java第五阶段_day06_数据结构\Java第五阶段_day06_数据结构\assets\679104911-5aee6a2083c64_articlex.png)]

完全二叉树

完全二叉树是一颗特殊的二叉树,它遵循以下规则:

假设完全二叉树高度为k,则完全二叉树需要符合以下两点:

  • 所有叶子节点都出现在k层或k-1层,并且从1~k-1层必须达到最大节点数。
  • 第k层可以是不满的,但是第k层的所有节点必须集中在最左边。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6VvrbWkU-1655426298730)(E:\就业冲刺资料\Java第五阶段_day06_数据结构\Java第五阶段_day06_数据结构\assets\1610953790434.png)]

二叉查找树

​ 二叉查找树(BinarySearch Tree,也叫二叉搜索树,或二叉排序树BinarySort Tree)或者是一棵空树,或者是具有下列性质的二叉树:

​ 1、若任意结点的左子树不空,则左子树上所有结点的值均小于它的根结点的值;

​ 2、若任意结点的右子树不空,则右子树上所有节点的值均大于它的根结点的值;

​ 3、任意结点的左右子树也分别为二叉查找树;

​ 4、没有键值相等的结点。

在实际应用中,二叉查找树的使用比较多。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MgLYCd6T-1655426298730)(E:\就业冲刺资料\Java第五阶段_day06_数据结构\Java第五阶段_day06_数据结构\assets\270932052801072.jpg)]

平衡二叉树

​ 平衡二叉树(AVL)是一种特殊的二叉查找树,其中每一个节点的左子树和右子树的高度差至多等于1。从平衡二叉树的名字中可以看出来,它是一种高度平衡的二叉排序树。那么什么叫做高度平衡呢?意思就是要么它是一颗空树,要么它的左子树和右子树都是平衡二叉树,且左子树和右子树的深度只差的绝对值绝对不超过1。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-zpCuemh9-1655426298730)(E:\就业冲刺资料\Java第五阶段_day06_数据结构\Java第五阶段_day06_数据结构\assets\1610955604683.png)]

红黑树

​ 红黑树(Red Black Tree) 是一种自平衡二叉查找树。

红黑树的特性
(1)每个节点或者是黑色,或者是红色。
(2)根节点是黑色。
(3)每个叶子节点(NIL)是黑色。
(4)如果一个节点是红色的,则它的子节点必须是黑色的。[注意:这里叶子节点,是指为空(NIL)的虚节点!]
(5)从一个节点到该节点的子孙节点的所有路径上包含相同数目的黑节点。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KH9zjxy4-1655426298731)(E:\就业冲刺资料\Java第五阶段_day06_数据结构\Java第五阶段_day06_数据结构\assets\251730074203156.jpg)]

​ 红黑树是自平衡的,当插入元素导致不满足红黑树的特性时,需要通过旋转变色来重新满足红黑树的特性,旋转分为左旋跟右旋。

红黑树跟平衡二叉树的区别

  1. 红黑树放弃了追求完全平衡,追求大致平衡,保证每次插入最多只需要三次旋转就能达到平衡,实现起来也更为简单。

  2. 平衡二叉树追求绝对平衡,条件比较苛刻,实现起来比较麻烦,每次插入新节点之后需要旋转的次数不能预知。

113、哈希表

哈希表(Hash table,也叫散列表),是根据关键码值(Key)而直接进行访问的数据结构。也就是说,它通过把关键码值映射到表中一个位置来访问记录,以加快查找的速度。这个映射函数叫做散列(哈希)函数,存放记录的数组叫做散列表。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-P6CEWJ3Y-1655426298731)(E:\就业冲刺资料\Java第五阶段_day06_数据结构\Java第五阶段_day06_数据结构\assets\u=2543259477,1307182566&fm=26&gp=0.jpg)]

散列函数

​ 一个好的散列函数应该让Key均匀的散列在数组中,常用的散列函数有:

  • 直接定址法

    ​ 取关键字或者关键字的某个线性函数为Hash地址,即address(key)=a*key+b;如知道学生的学号从2000开始,最大为4000,则可以将address(key)=key-2000作为hash地址。

  • 平方取中法

    ​ 对关键字进行平方运算,然后取结果中的中间几位作为hash地址,加入有以下关键字{421,423,436},平方之后的结果为{177241,178929,190096},那么可以取中间的两位数{ 72, 89, 00 }作为Hash地址。

  • 折叠法

    ​ 将关键字拆分成几部分,然后将这几部分组合在一起,以特定的方式进行转化形成Hash地址,假如知道图书的ISBN号为8903-241-23,可以将address(key)=89+03+24+12+3作为hash地址。

哈希冲突

​ 对应不同的关键字可能获得相同的hash地址,即 key1≠key2,但是f(key1)=f(key2)。这种现象就是冲突,而且这种冲突只能尽可能的减少,不能完全避免。

常用的冲突解决办法:

  • 开放地址法(线性探测法)

    当冲突法生时,继续往后查找数组的下一个空位,并将数据填入。比如1和101,1占据了一个位置,101进入时候就向下查找,找到下面的一个空位插入, 如果没有继续查找空位,直到找到为止并进行插入。

  • 链地址法(拉链法)

    将产生冲突的值以链表的形式连起来。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dsBCmacv-1655426298732)(E:\就业冲刺资料\Java第五阶段_day06_数据结构\Java第五阶段_day06_数据结构\assets\u=3294781550,3844432582&fm=26&gp=0.jpg)]

114、对比

每种数据结构有自己的特点跟优缺点,见如下表

数据结构 优 点 缺 点
数组 插入快 查找慢,删除慢,大小固定,只能存储单一元素
提供后进先出的存取方式 存取其他项很慢
队列 提供先进先出的存取方式 存取其他项很慢
链表 插入快,删除快 查找慢
二叉树 如果树是平衡的,则查找、插入、删除都快 删除算法复杂
红黑树 查找、删除、插入都快。树总是平衡 算法复杂
哈希表 如果关键字已知则存取极快 删除慢,如果不知道关键字存取慢,对存储空间使用不充分

算法

115、概述

算法是规则的有限集合,为解决特定问题而规定的一系列操作。

​ 在Java中,算法通常都是由类的方法来实现的。前面的数据结构,比如链表为啥插入、删除快,而 查找慢,平衡的二叉树插入、删除、查找都快,这都是实现这些数据结构的算法所造成的。后面我们讲 的各种排序实现也是算法范畴的重要领域。

116、算法的特征

  • 有穷性:有限步骤内正常结束,不能无限循环。
  • 确定性:每个步骤都必须有确定的含义,无歧义。
  • 可行性:原则上能精确进行,操作能通过有限次完成。
  • 输入:有0或多个输入。
  • 输出:至少有一个输出

117、数组的排序

​ JAVA中比较常用的算法有数组的排序,排序的算法有很多,常见的有冒泡排序、选择排序、插入排序、快速排序等。

118、冒泡排序

这个名词的由来很好理解,一般河水中的冒泡,水底刚冒出来的时候是比较小的,随着慢慢向水面 浮起会逐渐增大,这物理规律我不作过多解释,大家只需要了解即可。

冒泡算法的运作规律如下:

①、比较相邻的元素。如果第一个比第二个大,就交换他们两个。

②、对每一对相邻元素作同样的工作,从开始第一对到结尾的最后一对。这步做完后,最后的元素 会是最大的数(也就是第一波冒泡完成)。

③、针对所有的元素重复以上的步骤,除了最后一个。

④、持续每次对越来越少的元素重复上面的步骤,直到没有任何一对数字需要比较。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ySJDWFMK-1655426298732)(E:\就业冲刺资料\Java第五阶段_day07_算法\Java第五阶段_day07_算法\assets\冒泡排序.gif)]

代码实现

//冒泡排序
public class BubbleSort {public static void sort(int[] array) {// 这里for循环表示总共需要比较多少轮for (int i = 1; i < array.length; i++) {// 这里for循环表示每轮比较参与的元素下标// 对当前无序区间array[0 length-i]进行排序// j的范围很关键,这个范围是在逐步缩小的,因为每轮比较都会将最大的放在右边for (int j = 0; j < array.length - i; j++) {if (array[j] > array[j + 1]) {int temp = array[j];array[j] = array[j + 1];array[j + 1] = temp;}}}}public static void main(String[] args) {int[] array = { 4, 2, 8, 9, 5, 7, 6, 1, 3 };// 未排序数组顺序为System.out.println("未排序数组顺序为:");System.out.println(Arrays.toString(array));sort(array);System.out.println("经过冒泡排序后的数组顺序为:");System.out.println(Arrays.toString(array));}
}

119、选择排序

选择排序是每一次从待排序的数据元素中选出最小的一个元素,存放在序列的起始位置,直到全部待排序的数据元素排完。

分为三步:

①、从待排序序列中,找到关键字最小的元素

②、如果最小元素不是待排序序列的第一个元素,将其和第一个元素互换

③、从余下的 N - 1 个元素中,找出关键字最小的元素,重复(1)、(2)步,直到排序结束。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sxEAkBHW-1655426298733)(E:\就业冲刺资料\Java第五阶段_day07_算法\Java第五阶段_day07_算法\assets\选择排序.gif)]

代码实现

/ 选择排序
public class ChoiceSort {public static void sort(int[] array) {// 总共要经过N-1轮比较for (int i = 0; i < array.length - 1; i++) {int min = i;// 每轮需要比较的次数for (int j = i + 1; j < array.length; j++) {if (array[j] < array[min]) {min = j;// 记录目前能找到的最小值元素的下标}}// 将找到的最小值和i位置所在的值进行交换if (i != min) {int temp = array[i];array[i] = array[min];array[min] = temp;}}}public static void main(String[] args) {int[] array = { 4, 2, 8, 9, 5, 7, 6, 1, 3};// 未排序数组顺序为System.out.println("未排序数组顺序为:");System.out.println(Arrays.toString(array));sort(array);System.out.println("经过选择排序后的数组顺序为:");System.out.println(Arrays.toString(array));}
}

120、 插入排序

​ 直接插入排序基本思想是每一步将一个待排序的记录,插入到前面已经排好序的有序序列中去,直 到插完所有元素为止。

​ 插入排序还分为直接插入排序、二分插入排序、链表插入排序、希尔排序等等,这里我们只是以直接插入排序为例。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-0BRkZJjo-1655426298733)(E:\就业冲刺资料\Java第五阶段_day07_算法\Java第五阶段_day07_算法\assets\插入排序.gif)]

代码实现

//插入排序
public class InsertSort {public static int[] sort(int[] array) {// 从下标为1的元素开始选择合适的位置插入,因为下标为0的只有一个元素,默认是有序的for (int i = 1; i < array.length; i++) {int tmp = array[i];// 记录要插入的数据int j = i;while (j > 0 && array[j - 1]>tmp) {array[j] = array[j - 1];// 向后挪动j--;}array[j] = tmp;// 存在比其小的数,插入}return array;}public static void main(String[] args) {int[] array = { 4, 2, 8, 1, 3};// 未排序数组顺序为System.out.println("未排序数组顺序为:");System.out.println(Arrays.toString(array));sort(array);System.out.println("经过插入排序后的数组顺序为:");System.out.println(Arrays.toString(array));}}

121、快速排序

​ 快速排序是对冒泡排序的一种改进,该方法的基本思想是:

1.先从数列中取出一个数作为基准数。

2.分区过程,将比这个数大的数全放到它的右边,小于或等于它的数全放到它的左边。

3.再对左右区间重复第二步,直到各区间只有一个数。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-k9mQIlIp-1655426298734)(E:\就业冲刺资料\Java第五阶段_day07_算法\Java第五阶段_day07_算法\assets\快速排序.gif)]

代码实现

//快速排序
public class QuickSort {public static void main(String[] args) {int[] array = {  4, 2, 8, 9, 5, 7, 6, 1, 3};quickSort(array, 0, array.length - 1);System.out.println(Arrays.toString(array));}static void quickSort(int s[], int left, int right) {if (left < right) {int i = left, j = right, x = s[left];while (i < j) {while (i < j && s[j] >= x) { // 从右向左找第一个小于x的数j--;}while (i < j && s[i] <= x) {// 从左向右找第一个大于等于x的数i++;}//将找到的第一个小于x跟大于x的数据进行交换int temp = s[j];s[j] = s[i];s[i] = temp ;}
//          把目标交换到合适位置,保证前面的所有数都小于它,后面的数都大于它int temp = s[i] ;s[i] = s[left];s[left] = temp ;quickSort(s, left, i - 1); // 递归调用quickSort(s, i + 1, right);}}}

122、二分查找

​ 二分查找又叫做折半查找,其查找过程为:先确定待查记录所在的范围(区间),然后逐步缩小范围知道找到或者找不到该记录为止。注意二分查找是在有序表上进行的,且二分查找也是分治思想的很好例证。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-LlPeY8N5-1655426298734)(E:\就业冲刺资料\Java第五阶段_day07_算法\Java第五阶段_day07_算法\assets\461877-20160721092729169-843824718.gif)]

代码实现

public class BinarySearch {public static void main(String[] args) {int [] arr = {1,2,3,4,5,6,7,8,9,10};int result = binarySearch(arr,7);System.out.println(result);}public static int binarySearch(int [] srcArray, int des) {//定义初始最小、最大索引int start = 0;int end = srcArray.length - 1;//确保不会出现重复查找,越界while (start <= end) {//计算出中间索引值int middle = (end + start)>>>1 ;//防止溢出if (des == srcArray[middle]) {return middle;//判断下限} else if (des < srcArray[middle]) {end = middle - 1;//判断上限} else {start = middle + 1;}}//若没有,则返回-1return -1;}
}

java.util.Arrays已经实现了二分查找算法binarySearch,并重载了各种基本数据类型数组的查找,以int []为例。

代码演示

public class BinarySearch {public static void main(String[] args) {int [] arr = {1,2,3,4,5,6,7,8,9,10};// 在数组中查找目标为4的索引位置System.out.println(Arrays.binarySearch(arr, 4));}
}

设计模式

123、单例模式的几种实现方式

单例模式的实现有多种方式,如下所示:

1、懒汉式,线程不安全

**是否 Lazy 初始化:**是

**是否多线程安全:**否

**实现难度:**易

**描述:**这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
这种方式 lazy loading 很明显,不要求线程安全,在多线程不能正常工作。

代码实例:

public class Singleton {  private static Singleton instance;  private Singleton (){}  public static Singleton getInstance() {  if (instance == null) {  instance = new Singleton();  }  return instance;  }
}

接下来介绍的几种实现方式都支持多线程,但是在性能上有所差异。

2、懒汉式,线程安全

**是否 Lazy 初始化:**是

**是否多线程安全:**是

**实现难度:**易

**描述:**这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。
优点:第一次调用才初始化,避免内存浪费。
缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。
getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。

代码实例:

public class Singleton {  private static Singleton instance;  private Singleton (){}  public static synchronized Singleton getInstance() {  if (instance == null) {  instance = new Singleton();  }  return instance;  }
}

3、饿汉式

**是否 Lazy 初始化:**否

**是否多线程安全:**是

**实现难度:**易

**描述:**这种方式比较常用,但容易产生垃圾对象。
优点:没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
它基于 classloder 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

代码实例:

public class Singleton {  private static Singleton instance = new Singleton();  private Singleton (){}  public static Singleton getInstance() {  return instance;  }
}

4、双检锁/双重校验锁(DCL,即 double-checked locking)

**JDK 版本:**JDK1.5 起

**是否 Lazy 初始化:**是

**是否多线程安全:**是

**实现难度:**较复杂

**描述:**这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。

代码实例:

public class Singleton {  private volatile static Singleton singleton;  private Singleton (){}  public static Singleton getSingleton() {  if (singleton == null) {  synchronized (Singleton.class) {  if (singleton == null) {  singleton = new Singleton();  }  }  }  return singleton;  }
}

5、登记式/静态内部类

**是否 Lazy 初始化:**是

**是否多线程安全:**是

**实现难度:**一般

**描述:**这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloder 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有显示通过调用 getInstance 方法时,才会显示装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

代码实例:

public class Singleton {  private static class SingletonHolder {  private static final Singleton INSTANCE = new Singleton();  }  private Singleton (){}  public static final Singleton getInstance() {  return SingletonHolder.INSTANCE;  }
}

6、枚举

**JDK 版本:**JDK1.5 起

**是否 Lazy 初始化:**否

**是否多线程安全:**是

**实现难度:**易

**描述:**这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。

代码实例:

public enum Singleton {  INSTANCE;  public void whateverMethod() {  }
}

**经验之谈:**一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。
){}
public static Singleton getInstance() {
return instance;
}
}

**4、双检锁/双重校验锁(DCL,即 double-checked locking)****JDK 版本:**JDK1.5 起**是否 Lazy 初始化:**是**是否多线程安全:**是**实现难度:**较复杂**描述:**这种方式采用双锁机制,安全且在多线程情况下能保持高性能。
getInstance() 的性能对应用程序很关键。**代码实例:**

public class Singleton {
private volatile static Singleton singleton;
private Singleton (){}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}

**5、登记式/静态内部类****是否 Lazy 初始化:**是**是否多线程安全:**是**实现难度:**一般**描述:**这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。
这种方式同样利用了 classloder 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有显示通过调用 getInstance 方法时,才会显示装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。**代码实例:**

public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}

** 6、枚举 ****JDK 版本:**JDK1.5 起**是否 Lazy 初始化:**否**是否多线程安全:**是**实现难度:**易**描述:**这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
不能通过 reflection attack 来调用私有构造方法。**代码实例:**

public enum Singleton {
INSTANCE;
public void whateverMethod() {
}
}


** 经验之谈:**一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。

Java面试(第一组)相关推荐

  1. 第十二届蓝桥杯 2021年省赛真题 (Java 大学B组) 第一场

    蓝桥杯 2021年省赛真题 (Java 大学B组 ) #A ASC #B 卡片 朴素解法 弯道超车 #C 直线 直线方程集合 分式消除误差 平面几何 #D 货物摆放 暴力搜索 缩放质因子 #E 路径 ...

  2. 第十二届蓝桥杯 2021年省赛真题 (Java 大学A组) 第一场

    蓝桥杯 2021年省赛真题 (Java 大学A组 ) #A 相乘 朴素解法 同余方程 #B 直线 直线方程集合 分式消除误差 平面几何 #C 货物摆放 暴力搜索 缩放质因子 #D 路径 搜索 单源最短 ...

  3. 杭州美图JAVA面试(美图美妆app组)

    杭州美图JAVA面试(美图美妆app组) 办公环境地毯,但是看到工位也是没有卡位,总体拥挤. 技术总监面:30min 1.       Restful风格有哪些,有什么优点?http://www.cn ...

  4. java群面_【埃森哲Java面试】一共有三面吧,第一面群面,第二面技术吧。-看准网...

    其实对埃森哲不是很了解,在招聘网站上看到之后就填写了申请,本来因为没有对咨询有过经验,以为不会有网测之类的机会,结果就收到了网测,其实应该也不是海发,因为也有人没有收到,总而言之,网测通过之后就会收到 ...

  5. Java面试一百道题目(第一题)-什么是面向对象,谈谈你对面向对象的理解

    Java面试一百道题目(第一题) 1,什么是面向对象,谈谈你对面向对象的理解. 思路:用面向过程和面向对象做对比来突出什么是面向对象. 答:高级语言分为,面向对象语言和面向过程语言,面向过程语言,距离 ...

  6. 第十二届蓝桥杯大赛软件类省赛第一场 Java 大学 B 组题目蓝桥杯JavaB组大赛软件类省赛第十二届第一场

    第十二届蓝桥杯大赛软件类省赛第一场 Java 大学 B 组题目 在线看题 题目PDF下载链接 百度云 链接:https://pan.baidu.com/s/1LSZvUV5dFwNtSbOshORU1 ...

  7. java求职攻略_2020年求职攻略 《Java面试连成诀》教程免费分享

    原标题:2020年求职攻略 <Java面试连成诀>教程免费分享 IT行业薪资高.就业好.发展前景广阔,而面试是我们打开事业大门的第一关.技术的革新以及IT从业者增多加大了市场竞争,面试中问 ...

  8. java面试和笔试大全

    2.String是最基本的数据类型吗? 基本数据类型包括byte.int.char.long.float.double.boolean和short. java.lang.String类是final类型 ...

  9. 150个Java面试问答-最终清单(PDF下载)

    我们的Java面试问题和答案集合全都涉及可以在Java面试中使用的不同类型的问题,以使雇主可以测试您在Java和面向对象编程方面的技能. 在以下各节中,我们将讨论有关面向对象编程及其特性的Java面试 ...

  10. java核心面试_前100多个核心Java面试问题

    java核心面试 Core Java interview questions help you in preparing for java based interviews. Whether you ...

最新文章

  1. 在asp.net中读取XML文件信息的4种方法
  2. 2008年10月10日股票池
  3. 民生银行数据库智能运维的探索与实践
  4. 【论文投稿】计算机学科部分核心期刊投稿攻略
  5. Oracle 数据库字典 sys.obj$ 表中关于type#的解释
  6. 剑指offer之中序打印二叉树(非递归实现)
  7. j2ee核心模式_Operator和Sidecar正在成为软件交付新模式
  8. keep健身软件电脑版_电脑软件:优酷 (优化版)
  9. AI如何提升10倍筛药效率?6月18日华为云携手中科院上海药物所揭开谜底
  10. Android性能优化典范 - 第1季(番外:渲染)
  11. 2021-06-26初识JavaScript
  12. atitit uke企业行政部 世界行政区域划分表 与邮政编码规划 v5 r88.xlsx
  13. 5G关键技术与系统演进pdf
  14. 计算机如何连接wifi台式,无线网卡怎么连接台式电脑_台式机添加无线网的方法...
  15. Java多线程系列--【JUC集合05】- ConcurrentSkipListMap
  16. Jaspersoft Studio 报表模板设计
  17. spring boot读取resources下面的文件图片
  18. 2021年安全员-C证(陕西省)考试资料及安全员-C证(陕西省)新版试题
  19. j2me专业手机游戏开发基础
  20. 小程序weui组件使用

热门文章

  1. ubuntu安装完成后的相关配置
  2. 二分搜索算法的实现详解
  3. 《蓝桥杯备赛》CT117E嵌入式竞赛板LCD驱动库的使用(带完整源码)
  4. Struts2项目实战 微云盘(一):项目分析
  5. [完全背包问题]状态转移方程及其优化理解.
  6. pxcook(像素大厨)安装--
  7. qt 使用 xlsx库操作excel表格(代码准确性未验证)
  8. 第二十八节:隧道代理阿布云代理
  9. nyoj 708 ones
  10. TCP网络编程-三次握手建立连接-四次挥手断开连接