1. JVM、JRE和JDK的关系是什么?

JDK是(Java Development Kit)的缩写,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。
JRE是Java Runtime Environment缩写,它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。
JDK包含JRE,JRE包含JVM。
image.png

2. Java语言有哪些特点?

  • 面向对象(封装,继承,多态);
  • 平台无关性,平台无关性的具体表现在于,Java 是“一次编写,到处运行(Write Once,Run any Where)”的语言,因此采用 Java 语言编写的程序具有很好的可移植性,而保证这一点的正是 Java 的虚拟机机制。在引入虚拟机之后,Java 语言在不同的平台上运行不需要重新编译。
  • 可靠性、安全性;
  • 支持多线程。C++ 语言没有内置的多线程机制,因此必须调用操作系统的多线程功能来进行多线程程序设计,而 Java 语言却提供了多线程支持;
  • 支持网络编程并且很方便。Java 语言诞生本身就是为简化网络编程设计的,因此 Java 语言不仅支持网络编程而且很方便;
  • 编译与解释并存;

3. == 和 equals 的区别是什么?

== 解读
对于基本类型和引用类型 == 的作用效果是不同的,如下所示:

  • 基本类型:比较的是值是否相同;
  • 引用类型:比较的是引用是否相同;

代码示例:

  1. String a = "测试";
  2. String b = "测试";
  3. String c = new String("测试");
  4. String d = new String("测试");
  5. System.out.println(a==b);
  6. System.out.println(a==c);
  7. System.out.println(a.equals(b));
  8. System.out.println(a.equals(c));
  9. System.out.println(c.equals(d));

输出:
image.png
代码解读:因为 a和 b 指向的是同一个引用,所以 == 也是 true,而 new String()方法则重写开辟了内存空间,所以 == 结果为 false,而 equals 比较的一直是值,所以结果都为 true。
equals 解读
equals 本质上就是 ==,只不过 String 和 Integer 等重写了 equals 方法,把它变成了值比较。看下面的代码就明白了。
当我们进入 String 的 equals 方法,找到了答案,代码如下:

  1. public boolean equals(Object anObject) {
  2. //判断引用(地址值)是否相同,相同则返回true
  3. if (this == anObject) { //this代表当前类(String)调用equals方法的对象。
  4. return true;
  5. }
  6. //判断对象类型是否为String
  7. if (anObject instanceof String) {
  8. //如果判断出传入的参数的类型是String类型,把传入的参数强转为String类型
  9. String anotherString = (String)anObject;
  10. //value是类中字符数组的名字,通过value.length获得字符数组的长度
  11. //String的底层是由char类型的数组实现的。
  12. int n = value.length;
  13. //比较类中字符数组的长度与传入的参数对象中的字符数组长度是否一样
  14. if (n == anotherString.value.length) {
  15. char v1[] = value;//使用v1[]存储value的字串
  16. char v2[] = anotherString.value;//使用v2[]存储传入的参数中value的字串
  17. int i = 0;
  18. while (n-- != 0) {// 循环比较字符串的每一位
  19. if (v1[i] != v2[i])
  20. return false;// 不一样就返回false
  21. i++;
  22. }
  23. return true;// 每一位都一样 返回true
  24. }
  25. }
  26. //类型不一样返回false
  27. return false;
  28. }

原来是 String 重写了 Object 的 equals 方法,把引用比较改成了值比较。
总结 :== 对于基本类型来说是值比较,对于引用类型来说是比较的是引用(内存值);而 equals 默认情况下是引用比较,只是很多类重新了 equals 方法,比如 String、Integer 等把它变成了值比较,所以一般情况下 equals 比较的是值是否相等。
扩展:
String a=”hello world”; String b=”hello world”;
在java中有一个常量池,当创建String 类型的引用变量给它赋值时,java会到它的常量池中找”hello world”是不是在常量池中已存在。如果已经存在则返回这个常量池中的”hello world”的地址(在java中叫引用)给变量a 。注意a并不是一个对象,而是一个引用类型的变量。它里面存的实际上是一个地址值,而这个值是指向一个字符串对象的。在程序中凡是以”hello world”这种常量似的形式给出的都被放在常量池中。
a和b都是指向常量池的同一个常量字符串”hello world”的,因此它们返回的地址是相同的。
String a=new String(“hello world”); String b=new String(“hello world”);
这种用new关键字定义的字符串,是在堆中分配空间的。而分配空间就是由new去完成的,由new去决定分配多大空间,并对空间初始化为字符串”hello world” 返回其在堆上的地址。
new在堆中开辟了两块内存空间,返回的地址当然是不相等的了

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

不对,两个对象的 hashCode()相同,equals()不一定 true。
代码示例:

  1. String str1 = "通话";
  2. String str2 = "重地";
  3. System.out.println(String.format("str1:%d | str2:%d", str1.hashCode(),str2.hashCode()));
  4. System.out.println(str1.equals(str2));

执行的结果:
str1:1179395 | str2:1179395
false
代码解读:很显然“通话”和“重地”的 hashCode() 相同,然而 equals() 则为 false,因为在散列表中,hashCode()相等即两个键值对的哈希值相等,然而哈希值相等,并不一定能得出键值对相等。

5. switch 是否能作用在 byte 上,是否能作用在 long 上,是否能作用在 String 上?

Java5 以前 switch(expr)中,expr 只能是 byte、short、char、int。
从 Java 5 开始,Java 中引入了枚举类型, expr 也可以是 enum 类型。
从 Java 7 开始,expr还可以是字符串(String),但是长整型(long)在目前所有的版本中都是不可以的。

6. break ,continue ,return 的区别及作用?

  • break 跳出总上一层循环,不再执行循环(结束当前的循环体)
  • continue 跳出本次循环,继续执行下次循环(结束正在执行的循环 进入下一个循环条件)
  • return 程序返回,不再执行下面的代码(结束当前的方法 直接返回)

7. 为什么要用static关键字?

通常来说,用new创建类的对象时,数据存储空间才被分配,方法才供外界调用。但有时我们只想为特定域分配单一存储空间,不考虑要创建多少对象或者说根本就不创建任何对象,再就是我们想在没有创建对象的情况下也想调用方法。在这两种情况下,static关键字,满足了我们的需求。

8. ”static”关键字是什么意思?Java中是否可以覆盖(override)一个private或者是static的方法?

“static”关键字表明一个成员变量或者是成员方法可以在没有所属的类的实例变量的情况下被访问。
Java中static方法不能被覆盖,因为方法覆盖是基于运行时动态绑定的,而static方法是编译时静态绑定的。static方法跟类的任何实例都不相关,所以概念上不适用。

9. 是否可以在static环境中访问非static变量?

static变量在Java中是属于类的,它在所有的实例中的值是一样的。当类被Java虚拟机载入的时候,会对static变量进行初始化。如果你的代码尝试不用实例来访问非static的变量,编译器会报错,因为这些变量还没有被创建出来,还没有跟任何实例关联上。

10. static静态方法能不能引用非静态资源?

不能,new的时候才会产生的东西,对于初始化后就存在的静态资源来说,根本不认识它。

11. static静态方法里面能不能引用静态资源?

可以,因为都是类初始化的时候加载的,大家相互都认识。

12. 非静态方法里面能不能引用静态资源?

可以,非静态方法就是实例方法,那是new之后才产生的,那么属于类的内容它都认识。

13. java静态变量、代码块、和静态方法的执行顺序是什么?

基本上代码块分为三种:Static静态代码块、构造代码块、普通代码块
代码块执行顺序静态代码块——> 构造代码块 ——> 构造函数——> 普通代码块
继承中代码块执行顺序:父类静态块——>子类静态块——>父类代码块——>父类构造器——>子类代码块——>子类构造器

14. final 在 java 中有什么作用?

  • final 修饰的类叫最终类,该类不能被继承。
  • final 修饰的方法不能被重写。
  • final 修饰的变量叫常量,常量必须初始化,初始化之后值就不能被修改。

15. java 中的 Math.round(-1.5) 等于多少?

等于 -1,因为在数轴上取值时,中间值(0.5)向右取整,所以正 0.5 是往上取整,负 0.5 是直接舍弃。

16. String 属于基础的数据类型吗?

String 不属于基础类型,基础类型有 8 种:byte、boolean、char、short、int、float、long、double,而 String 属于对象。

17.String为什么要设计成不可变的?

1.便于实现字符串池(String pool)
在Java中,由于会大量的使用String常量,如果每一次声明一个String都创建一个String对象,那将会造成极大的空间资源的浪费。Java提出了String pool的概念,在堆中开辟一块存储空间String pool,当初始化一个String变量时,如果该字符串已经存在了,就不会去创建一个新的字符串变量,而是会返回已经存在了的字符串的引用。
String a = “Hello world!”; String b = “Hello world!”;
如果字符串是可变的,某一个字符串变量改变了其值,那么其指向的变量的值也会改变,String pool将不能够实现!
2.使多线程安全
在并发场景下,多个线程同时读一个资源,是安全的,不会引发竞争,但对资源进行写操作时是不安全的,不可变对象不能被写,所以保证了多线程的安全。
3.避免安全问题
在网络连接和数据库连接中字符串常常作为参数,例如,网络连接地址URL,文件路径path,反射机制所需要的String参数。其不可变性可以保证连接的安全性。如果字符串是可变的,黑客就有可能改变字符串指向对象的值,那么会引起很严重的安全问题。
4.加快字符串处理速度
由于String是不可变的,保证了hashcode的唯一性,于是在创建对象时其hashcode就可以放心的缓存了,不需要重新计算。这也就是Map喜欢将String作为Key的原因,处理速度要快过其它的键对象。所以HashMap中的键往往都使用String。
总体来说,String不可变的原因要包括 设计考虑,效率优化,以及安全性这三大方面。

18. 字符型常量和字符串常量的区别?

  1. 形式上: 字符常量是单引号引起的一个字符,字符串常量是双引号引起的若干个字符;
  2. 含义上: 字符常量相当于一个整型值( ASCII 值),可以参加表达式运算;字符串常量代表一个地址值(该字符串在内存中存放位置,相当于对象;
  3. 占内存大小:字符常量只占2个字节;字符串常量占若干个字节(至少一个字符结束标志) (注意: char 在Java中占两个字节)。

19. 什么是字符串常量池?

java中常量池的概念主要有三个:全局字符串常量池,class文件常量池,运行时常量池。我们现在所说的就是全局字符串常量池,对这个想弄明白的同学可以看这篇Java中几种常量池的区分。
jvm为了提升性能和减少内存开销,避免字符的重复创建,其维护了一块特殊的内存空间,即字符串池,当需要使用字符串时,先去字符串池中查看该字符串是否已经存在,如果存在,则可以直接使用,如果不存在,初始化,并将该字符串放入字符串常量池中。
字符串常量池的位置也是随着jdk版本的不同而位置不同。在jdk6中,常量池的位置在永久代(方法区)中,此时常量池中存储的是对象。在jdk7中,常量池的位置在堆中,此时,常量池存储的就是引用了。在jdk8中,永久代(方法区)被元空间取代了。

20. java 中操作字符串都有哪些类?它们之间有什么区别?

操作字符串的类有:String、StringBuffer、StringBuilder。
String 和 StringBuffer、StringBuilder 的区别在于 String 声明的是不可变的对象,每次操作都会生成新的 String 对象,然后将指针指向新的 String 对象,而 StringBuffer、StringBuilder 可以在原有对象的基础上进行操作,所以在经常改变字符串内容的情况下最好不要使用 String。
StringBuffer 和 StringBuilder 最大的区别在于,StringBuffer 是线程安全的,而 StringBuilder 是非线程安全的,但 StringBuilder 的性能却高于 StringBuffer,所以在单线程环境下推荐使用 StringBuilder,多线程环境下推荐使用 StringBuffer。

21. String str=”i”与 String str=new String(“i”)一样吗?

不一样,因为内存的分配方式不一样。String str=”i”的方式,java 虚拟机会将其分配到常量池中;而 String str=new String(“i”) 则会被分到堆内存中。

22. 如何将字符串反转?

使用 StringBuilder 或者 stringBuffer 的 reverse() 方法。
示例代码:

  1. // StringBuffer reverse
  2. StringBuffer stringBuffer = new StringBuffer();
  3. stringBuffer.append("abcdefg");
  4. System.out.println(stringBuffer.reverse()); // gfedcba
  5. // StringBuilder reverse
  6. StringBuilder stringBuilder = new StringBuilder();
  7. stringBuilder.append("abcdefg");
  8. System.out.println(stringBuilder.reverse()); // gfedcba

23. String 类的常用方法都有那些?

  • indexOf():返回指定字符的索引。
  • charAt():返回指定索引处的字符。
  • replace():字符串替换。
  • trim():去除字符串两端空白。
  • split():分割字符串,返回一个分割后的字符串数组。
  • getBytes():返回字符串的 byte 类型数组。
  • length():返回字符串长度。
  • toLowerCase():将字符串转成小写字母。
  • toUpperCase():将字符串转成大写字符。
  • substring():截取字符串。
  • equals():字符串比较。

24. 抽象类必须要有抽象方法吗?

不需要,抽象类不一定非要有抽象方法。
示例代码:

  1. abstract class Cat {
  2. public static void sayHi() {
  3. System.out.println("hi~");
  4. }
  5. }

上面代码,抽象类并没有抽象方法但完全可以正常运行。

25. 普通类和抽象类有哪些区别?

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

26. 抽象类能使用 final 修饰吗?

不能,定义抽象类就是让其他类继承的,如果定义为 final 该类就不能被继承,这样彼此就会产生矛盾,所以 final 不能修饰抽象类,如下图所示,编辑器也会提示错误信息:
JAVA基础100问 - 图3

27. 接口和抽象类有什么区别?

  • 实现:抽象类的子类使用 extends 来继承;接口必须使用 implements 来实现接口。
  • 构造函数:抽象类可以有构造函数;接口不能有。
  • main 方法:抽象类可以有 main 方法,并且我们能运行它;接口不能有 main 方法。
  • 实现数量:类可以实现很多个接口;但是只能继承一个抽象类。
  • 访问修饰符:接口中的方法默认使用 public 修饰;抽象类中的方法可以是任意访问修饰符。

28. 面向对象和面向过程的区别?

面向过程

  • 优点:性能比面向对象高,因为类调用时需要实例化,开销比较大,比较消耗资源;比如单片机、嵌入式开发、Linux/Unix等一般采用面向过程开发,性能是最重要的因素。
  • 缺点:没有面向对象易维护、易复用、易扩展。

面向对象

  • 优点:易维护、易复用、易扩展,由于面向对象有封装、继承、多态性的特性,可以设计出低耦合的系统,使系统更加灵活、更加易于维护。
  • 缺点:性能比面向过程低。

29. 讲讲面向对象三大特性

  • 封装。封装最好理解了。封装是面向对象的特征之一,是对象和类概念的主要特性。封装,也就是把客观事物封装成抽象的类,并且类可以把自己的数据和方法只让可信的类或者对象操作,对不可信的进行信息隐藏。
  • 继承。继承是指这样一种能力:它可以使用现有类的所有功能,并在无需重新编写原来的类的情况下对这些功能进行扩展。通过继承创建的新类称为“子类”或“派生类”,被继承的类称为“基类”、“父类”或“超类”。
  • 多态性。它是指在父类中定义的属性和方法被子类继承之后,可以具有不同的数据类型或表现出不同的行为,这使得同一个属性或方法在父类及其各个子类中具有不同的含义。

30. Java语言是如何实现多态的?

本质上多态分两种:
1、编译时多态(又称静态多态)
2、运行时多态(又称动态多态)

重载(overload)就是编译时多态的一个例子,编译时多态在编译时就已经确定,运行的时候调用的是确定的方法。
我们通常所说的多态指的都是运行时多态,也就是编译时不确定究竟调用哪个具体方法,一直延迟到运行时才能确定。这也是为什么有时候多态方法又被称为延迟方法的原因。
Java实现多态有 3 个必要条件:继承、重写和向上转型。只有满足这 3 个条件,开发人员才能够在同一个继承结构中使用统一的逻辑实现代码处理不同的对象,从而执行不同的行为。

  • 继承:在多态中必须存在有继承关系的子类和父类。
  • 重写:子类对父类中某些方法进行重新定义,在调用这些方法时就会调用子类的方法。
  • 向上转型:在多态中需要将子类的引用赋给父类对象,只有这样该引用才既能可以调用父类的方法,又能调用子类的方法。


31. 重载(Overload)和重写(Override)的区别是什么?

方法的重载和重写都是实现多态的方式,区别在于前者实现的是编译时的多态性,而后者实现的是运行时的多态性。

  • 重写发生在子类与父类之间, 重写方法返回值和形参都不能改变,与方法返回值和访问修饰符无关,即重载的方法不能根据返回类型进行区分。即外壳不变,核心重写!
  • 重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。最常用的地方就是构造器的重载。

image.png

32. 重载的方法能否根据返回值类型进行区分?

不能根据返回值类型来区分重载的方法。因为调用时不指定类型信息,编译器不知道你要调用哪个函数。
float max(int a, int b); int max(int a, int b);
当调用max(1,2);时无法确定调用的是哪个,单从这一点上来说,仅返回值类型不同的重载是不应该允许的。

33. 构造器(constructor)是否可被重写(override)?

构造器不能被继承,因此不能被重写,但可以被重载。每一个类必须有自己的构造函数,负责构造自己这部分的构造。子类不会覆盖父类的构造函数,相反必须一开始调用父类的构造函数。

34. 介绍下hashCode()?

hashCode() 的作用是获取哈希码,也称为散列码;它实际上是返回一个int整数。这个哈希码的作用是确定该对象在哈希表中的索引位置。hashCode() 定义在JDK的Object.java中,这就意味着Java中的任何类都包含有hashCode()函数。
散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)

35. 为什么要有 hashCode?

以“HashSet 如何检查重复”为例子来说明为什么要有 hashCode
当你把对象加入 HashSet 时,HashSet 会先计算对象的 hashcode 值来判断对象加入的位置,同时也会与其他已经加入的对象的 hashcode 值作比较,如果没有相符的hashcode,HashSet会假设对象没有重复出现。
但是如果发现有相同 hashcode 值的对象,这时会调用 equals()方法来检查 hashcode 相等的对象是否真的相同。如果两者相同,HashSet 就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了 equals 的次数,相应就大大提高了执行速度。

36. hashCode(),equals()两种方法是什么关系?

Java 对于 eqauls() 方法和 hashCode() 方法是这样规定的:

  • 同一对象上多次调用 hashCode() 方法,总是返回相同的整型值。
  • 如果 a.equals(b),则一定有 a.hashCode() 一定等于 b.hashCode()。
  • 如果 !a.equals(b),则 a.hashCode() 不一定等于 b.hashCode()。此时如果 a.hashCode() 总是不等于 b.hashCode(),会提高 hashtables 的性能。
  • a.hashCode()==b.hashCode() 则 a.equals(b) 可真可假
  • a.hashCode()!= b.hashCode() 则 a.equals(b) 为假。

上面结论简记:

  • 如果两个对象 equals,Java 运行时环境会认为他们的 hashCode 一定相等。
  • 如果两个对象不 equals,他们的 hashCode 有可能相等。
  • 如果两个对象 hashCode 相等,他们不一定 equals。
  • 如果两个对象 hashCode 不相等,他们一定不 equals

补充:关于 equals() 和 hashCode() 的重要规范

  • 规范1:若重写 equals() 方法,有必要重写 hashcode()方法,确保通过 equals()方法判断结果为 true 的两个对象具备相等的 hashcode() 方法返回值。说得简单点就是:“如果两个对象相同,那么他们的 hashCode 应该相等”。不过请注意:这个只是规范,如果非要写一个类让 equals() 方法返回 true 而 hashCode() 方法返回两个不相等的值,编译和运行都是不会报错的。不过这样违反了 Java 规范,程序也就埋下了 BUG。
  • 规范2:如果 equals() 方法返回 false,即两个对象“不相同”,并不要求对这两个对象调用 hashCode() 方法得到两个不相同的数。说的简单点就是:“如果两个对象不相同,他们的 hashCode 可能相同”。

  • 37. 为什么重写 equals 方法必须重写 hashcode 方法 ?

    断的时候先根据hashcode进行的判断,相同的情况下再根据equals()方法进行判断。如果只重写了equals方法,而不重写hashcode的方法,会造成hashcode的值不同,而equals()方法判断出来的结果为true。
    在Java中的一些容器中,不允许有两个完全相同的对象,插入的时候,如果判断相同则会进行覆盖。这时候如果只重写了equals()的方法,而不重写hashcode的方法,Object中hashcode是根据对象的存储地址转换而形成的一个哈希值。这时候就有可能因为没有重写hashcode方法,造成相同的对象散列到不同的位置而造成对象的不能覆盖的问题。

    38. 在使用 HashMap 的时候,用 String 做 key 有什么好处?

    HashMap 内部实现是通过 key 的 hashcode 来确定 value 的存储位置,因为字符串是不可变的,所以当创建字符串时,它的 hashcode 被缓存下来,不需要再次计算,所以相比于其他对象更快。

39. 包装类型是什么?基本类型和包装类型有什么区别?

Java 为每一个基本数据类型都引入了对应的包装类型(wrapper class),int 的包装类就是 Integer,从 Java 5 开始引入了自动装箱/拆箱机制,把基本类型转换成包装类型的过程叫做装箱(boxing);反之,把包装类型转换成基本类型的过程叫做拆箱(unboxing),使得二者可以相互转换。
Java 为每个原始类型提供了包装类型:
原始类型: boolean,char,byte,short,int,long,float,double
包装类型:Boolean,Character,Byte,Short,Integer,Long,Float,Double
基本类型和包装类型的区别主要有以下 几点

  • 包装类型可以为 null,而基本类型不可以。它使得包装类型可以应用于 POJO 中,而基本类型则不行。那为什么 POJO 的属性必须要用包装类型呢?《阿里巴巴 Java 开发手册》上有详细的说明, 数据库的查询结果可能是 null,如果使用基本类型的话,因为要自动拆箱(将包装类型转为基本类型,比如说把 Integer 对象转换成 int 值),就会抛出 NullPointerException 的异常。
  • 包装类型可用于泛型,而基本类型不可以。泛型不能使用基本类型,因为使用基本类型时会编译出错。
    List list = new ArrayList<>(); // 提示 Syntax error, insert “Dimensions” to complete ReferenceType
    List list = new ArrayList<>();

因为泛型在编译时会进行类型擦除,最后只保留原始类型,而原始类型只能是 Object 类及其子类——基本类型是个特例。

  • 基本类型比包装类型更高效。基本类型在栈中直接存储的具体数值,而包装类型则存储的是堆中的引用。 很显然,相比较于基本类型而言,包装类型需要占用更多的内存空间。

40. 解释一下自动装箱和自动拆箱?

自动装箱:将基本数据类型重新转化为对象
public class Test { public static void main(String[] args) { // 声明一个Integer对象,用到了自动的装箱:解析为:Integer num = Integer.valueOf(9); Integer num = 9; } }
9是属于基本数据类型的,原则上它是不能直接赋值给一个对象Integer的。但jdk1.5 开始引入了自动装箱/拆箱机制,就可以进行这样的声明,自动将基本数据类型转化为对应的封装类型,成为一个对象以后就可以调用对象所声明的所有的方法。
自动拆箱:将对象重新转化为基本数据类型
public class Test { public static void main(String[] args) { / /声明一个Integer对象 Integer num = 9; // 进行计算时隐含的有自动拆箱 System.out.print(num—); } }
因为对象时不能直接进行运算的,而是要转化为基本数据类型后才能进行加减乘除

41. int 和 Integer 有什么区别?

  • Integer是int的包装类;int是基本数据类型;
  • Integer变量必须实例化后才能使用;int变量不需要;
  • Integer实际是对象的引用,指向此new的Integer对象;int是直接存储数据值 ;
  • Integer的默认值是null;int的默认值是0。

42. 两个new生成的Integer变量的对比

由于Integer变量实际上是对一个Integer对象的引用,所以两个通过new生成的Integer变量永远是不相等的(因为new生成的是两个对象,其内存地址不同)。
Integer i = new Integer(10000); Integer j = new Integer(10000); System.out.print(i == j); //false

43. Integer变量和int变量的对比

Integer变量和int变量比较时,只要两个变量的值是向等的,则结果为true(因为包装类Integer和基本数据类型int比较时,java会自动拆包装为int,然后进行比较,实际上就变为两个int变量的比较)
int a = 10000; Integer b = new Integer(10000); Integer c=10000; System.out.println(a == b); // true System.out.println(a == c); // true

44. 非new生成的Integer变量和new Integer()生成变量的对比

非new生成的Integer变量和new Integer()生成的变量比较时,结果为false。(因为非new生成的Integer变量指向的是java常量池中的对象,而new Integer()生成的变量指向堆中新建的对象,两者在内存中的地址不同)
Integer b = new Integer(10000); Integer c=10000; System.out.println(b == c); // false

45. 两个非new生成的Integer对象的对比

对于两个非new生成的Integer对象,进行比较时,如果两个变量的值在区间-128到127之间,则比较结果为true,如果两个变量的值不在此区间,则比较结果为false
Integer i = 100; Integer j = 100; System.out.print(i == j); //true Integer i = 128; Integer j = 128; System.out.print(i == j); //false
当值在 -128 ~ 127之间时,java会进行自动装箱,然后会对值进行缓存,如果下次再有相同的值,会直接在缓存中取出使用。缓存是通过Integer的内部类IntegerCache来完成的。当值超出此范围,会在堆中new出一个对象来存储。
给一个Integer对象赋一个int值的时候,会调用Integer类的静态方法valueOf,源码如下:
public static Integer valueOf(String s, int radix) throws NumberFormatException { return Integer.valueOf(parseInt(s,radix)); } /* (1)在-128~127之内:静态常量池中cache数组是static final类型,cache数组对象会被存储于静态常量池中。 cache数组里面的元素却不是static final类型,而是cache[k] = new Integer(j++), 那么这些元素是存储于堆中,只是cache数组对象存储的是指向了堆中的Integer对象(引用地址) (2)在-128~127 之外:新建一个 Integer对象,并返回。 */ public static Integer valueOf(int i) { assert IntegerCache.high >= 127; if (i >= IntegerCache.low && i <= IntegerCache.high) { return IntegerCache.cache[i + (-IntegerCache.low)]; } return new Integer(i); }
IntegerCache是Integer的内部类,源码如下:
image.png

46. 什么是反射?

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

47. 反射机制的优缺点有哪些?

优点:能够运行时动态获取类的实例,提高灵活性;可与动态编译结合Class.forName(‘com.mysql.jdbc.Driver.class’);,加载MySQL的驱动类。
缺点:使用反射性能较低,需要解析字节码,将内存中的对象进行解析。其解决方案是:通过setAccessible(true)关闭JDK的安全检查来提升反射速度;多次创建一个类的实例时,有缓存会快很多;ReflflectASM工具类,通过字节码生成的方式加快反射速度。

48. 如何获取反射中的Class对象?

  1. Class.forName(“类的路径”);当你知道该类的全路径名时,你可以使用该方法获取 Class 类对象。
    Class clz = Class.forName(“java.lang.String”);

  2. 类名.class。这种方法只适合在编译前就知道操作的 Class。
    Class clz = String.class;

  3. 对象名.getClass()。
    String str = new String(“Hello”);
    Class clz = str.getClass();

  4. 如果是基本类型的包装类,可以调用包装类的Type属性来获得该包装类的Class对象。

49. Java反射API有几类?

反射 API 用来生成 JVM 中的类、接口或则对象的信息。

  • Class 类:反射的核心类,可以获取类的属性,方法等信息。
  • Field 类:Java.lang.reflec 包中的类,表示类的成员变量,可以用来获取和设置类之中的属性值。
  • Method 类:Java.lang.reflec 包中的类,表示类的方法,它可以用来获取类中的方法信息或者执行方法。
  • Constructor 类:Java.lang.reflec 包中的类,表示类的构造方法。

    50. 反射使用的步骤?

  1. 获取想要操作的类的Class对象,这是反射的核心,通过Class对象我们可以任意调用类的方法。
  2. 调用 Class 类中的方法,既就是反射的使用阶段。
  3. 使用反射 API 来操作这些信息。

具体可以看下面的例子:
public class Apple { private int price; public int getPrice() { return price; } public void setPrice(int price) { this.price = price; } public static void main(String[] args) throws Exception{ //正常的调用 Apple apple = new Apple(); apple.setPrice(5); System.out.println(“Apple Price:” + apple.getPrice()); //使用反射调用 Class clz = Class.forName(“com.chenshuyi.api.Apple”); Method setPriceMethod = clz.getMethod(“setPrice”, int.class); Constructor appleConstructor = clz.getConstructor(); Object appleObj = appleConstructor.newInstance(); setPriceMethod.invoke(appleObj, 14); Method getPriceMethod = clz.getMethod(“getPrice”); System.out.println(“Apple Price:” + getPriceMethod.invoke(appleObj)); } }
从代码中可以看到我们使用反射调用了 setPrice 方法,并传递了 14 的值。之后使用反射调用了 getPrice 方法,输出其价格。上面的代码整个的输出结果是:
Apple Price:5 Apple Price:14
从这个简单的例子可以看出,一般情况下我们使用反射获取一个对象的步骤:

  • 获取类的 Class 对象实例

Class clz = Class.forName(“com.zhenai.api.Apple”);

  • 根据 Class 对象实例获取 Constructor 对象

Constructor appleConstructor = clz.getConstructor();

  • 使用 Constructor 对象的 newInstance 方法获取反射类对象

Object appleObj = appleConstructor.newInstance();
而如果要调用某一个方法,则需要经过下面的步骤:

  • 获取方法的 Method 对象

Method setPriceMethod = clz.getMethod(“setPrice”, int.class);

  • 利用 invoke 方法调用方法

setPriceMethod.invoke(appleObj, 14);

51. 为什么引入反射概念?反射机制的应用有哪些?

我们来看一下 Oracle 官方文档中对反射的描述:
从 Oracle 官方文档中可以看出,反射主要应用在以下几方面:

  • 反射让开发人员可以通过外部类的全路径名创建对象,并使用这些类,实现一些扩展的功能。
  • 反射让开发人员可以枚举出类的全部成员,包括构造函数、属性、方法。以帮助开发者写出正确的代码。
  • 测试时可以利用反射 API 访问类的私有成员,以保证测试代码覆盖率。

也就是说,Oracle 希望开发者将反射作为一个工具,用来帮助程序员实现本不可能实现的功能。
举两个最常见使用反射的例子,来说明反射机制的强大之处:
第一种:JDBC 的数据库的连接
在JDBC 的操作中,如果要想进行数据库的连接,则必须按照以上的几步完成

  1. 通过Class.forName()加载数据库的驱动程序 (通过反射加载,前提是引入相关了Jar包);
  2. 通过 DriverManager 类进行数据库的连接,连接的时候要输入数据库的连接地址、用户名、密码;
  3. 通过Connection 接口接收连接。

public class ConnectionJDBC { / @param args / //驱动程序就是之前在classpath中配置的JDBC的驱动程序的JAR 包中 public static final String DBDRIVER = “com.mysql.jdbc.Driver”; //连接地址是由各个数据库生产商单独提供的,所以需要单独记住 public static final String DBURL = “jdbc:mysql://localhost:3306/test”; //连接数据库的用户名 public static final String DBUSER = “root”; //连接数据库的密码 public static final String DBPASS = “”; public static void main(String[] args) throws Exception { Connection con = null; //表示数据库的连接对象 Class.forName(DBDRIVER); //1、使用CLASS 类加载驱动程序 ,反射机制的体现 con = DriverManager.getConnection(DBURL,DBUSER,DBPASS); //2、连接数据库 System.out.println(con); con.close(); // 3、关闭数据库 }
第二种:
Spring 框架的使用,最经典的就是xml的配置模式**。
Spring 通过 XML 配置模式装载 Bean 的过程:

  1. 将程序内所有 XML 或 Properties 配置文件加载入内存中;
  2. Java类里面解析xml或properties里面的内容,得到对应实体类的字节码字符串以及相关的属性信息;
  3. 使用反射机制,根据这个字符串获得某个类的Class实例;
  4. 动态配置实例的属性。

Spring这样做的好处是:

  • 不用每一次都要在代码里面去new或者做其他的事情;
  • 以后要改的话直接改配置文件,代码维护起来就很方便了;
  • 有时为了适应某些需求,Java类里面不一定能直接调用另外的方法,可以通过反射机制来实现。

模拟 Spring 加载 XML 配置文件:
image.png

52. 反射机制的原理是什么?

Class actionClass=Class.forName(“MyClass”); Object action=actionClass.newInstance(); Method method = actionClass.getMethod(“myMethod”,null); method.invoke(action,null);
上面就是最常见的反射使用的例子,前两行实现了类的装载、链接和初始化(newInstance方法实际上也是使用反射调用了方法),后两行实现了从class对象中获取到method对象然后执行反射调用。
因反射原理较复杂,下面简要描述下流程,想要详细了解的小伙伴,可以看这篇文章:https://www.cnblogs.com/yougewe/p/10125073.html

  1. 反射获取类实例 Class.forName(),并没有将实现留给了java,而是交给了jvm去加载!主要是先获取 ClassLoader, 然后调用 native 方法,获取信息,加载类则是回调 java.lang.ClassLoader。最后,jvm又会回调 ClassLoader 进类加载!
  2. newInstance() 主要做了三件事:
  • 权限检测,如果不通过直接抛出异常;
  • 查找无参构造器,并将其缓存起来;
  • 调用具体方法的无参构造方法,生成实例并返回。
  • 获取Method对象,

image.png

上面的Class对象是在加载类时由JVM构造的,JVM为每个类管理一个独一无二的Class对象,这份Class对象里维护着该类的所有Method,Field,Constructor的cache,这份cache也可以被称作根对象。
每次getMethod获取到的Method对象都持有对根对象的引用,因为一些重量级的Method的成员变量(主要是MethodAccessor),我们不希望每次创建Method对象都要重新初始化,于是所有代表同一个方法的Method对象都共享着根对象的MethodAccessor,每一次创建都会调用根对象的copy方法复制一份:
Method copy() { Method res = new Method(clazz, name, parameterTypes, returnType, exceptionTypes, modifiers, slot, signature, annotations, parameterAnnotations, annotationDefault); res.root = this; res.methodAccessor = methodAccessor; return res; }

  1. 调用invoke()方法。调用invoke方法的流程如下:

image.png

调用Method.invoke之后,会直接去调MethodAccessor.invoke。MethodAccessor就是上面提到的所有同名method共享的一个实例,由ReflectionFactory创建。
创建机制采用了一种名为inflation的方式(JDK1.4之后):如果该方法的累计调用次数<=15,会创建出NativeMethodAccessorImpl,它的实现就是直接调用native方法实现反射;如果该方法的累计调用次数>15,会由java代码创建出字节码组装而成的MethodAccessorImpl。(是否采用inflation和15这个数字都可以在jvm参数中调整)
以调用MyClass.myMethod(String s)为例,生成出的MethodAccessorImpl字节码翻译成Java代码大致如下:
public class GeneratedMethodAccessor1 extends MethodAccessorImpl { public Object invoke(Object obj, Object[] args) throws Exception { try { MyClass target = (MyClass) obj; String arg0 = (String) args[0]; target.myMethod(arg0); } catch (Throwable t) { throw new InvocationTargetException(t); } } }

53. Java中的泛型是什么 ?

泛型是 JDK1.5 的一个新特性,泛型就是将类型参数化,其在编译时才确定具体的参数。这种参数类型可以用在类、接口和方法的创建中,分别称为泛型类、泛型接口、泛型方法。

54. 使用泛型的好处是什么?

远在 JDK 1.4 版本的时候,那时候是没有泛型的概念的,如果使用 Object 来实现通用、不同类型的处理,有这么两个缺点:

  1. 每次使用时都需要强制转换成想要的类型
  2. 在编译时编译器并不知道类型转换是否正常,运行时才知道,不安全。

如这个例子:
List list = new ArrayList(); list.add(“www.cnblogs.com”); list.add(23); String name = (String)list.get(0); String number = (String)list.get(1); //ClassCastException
上面的代码在运行时会发生强制类型转换异常。这是因为我们在存入的时候,第二个是一个 Integer 类型,但是取出来的时候却将其强制转换为 String 类型了。Sun 公司为了使 Java 语言更加安全,减少运行时异常的发生。于是在 JDK 1.5 之后推出了泛型的概念。
根据《Java 编程思想》中的描述,泛型出现的动机在于:有许多原因促成了泛型的出现,而最引人注意的一个原因,就是为了创建容器类
使用泛型的好处有以下几点

  1. 类型安全
  • 泛型的主要目标是提高 Java 程序的类型安全
  • 编译时期就可以检查出因 Java 类型不正确导致的 ClassCastException 异常
  • 符合越早出错代价越小原则
  • 消除强制类型转换
  • 泛型的一个附带好处是,使用时直接得到目标类型,消除许多强制类型转换
  • 所得即所需,这使得代码更加可读,并且减少了出错机会
  • 潜在的性能收益
  • 由于泛型的实现方式,支持泛型(几乎)不需要 JVM 或类文件更改
  • 所有工作都在编译器中完成
  • 编译器生成的代码跟不使用泛型(和强制类型转换)时所写的代码几乎一致,只是更能确保类型安全而已

    55. Java泛型的原理是什么 ? 什么是类型擦除 ?

    泛型是一种语法糖,泛型这种语法糖的基本原理是类型擦除。Java中的泛型基本上都是在编译器这个层次来实现的,也就是说:泛型只存在于编译阶段,而不存在于运行阶段。在编译后的 class 文件中,是没有泛型这个概念的。
    类型擦除:使用泛型的时候加上的类型参数,编译器在编译的时候去掉类型参数。
    例如:
    public class Caculate { private T num; }
    我们定义了一个泛型类,定义了一个属性成员,该成员的类型是一个泛型类型,这个 T 具体是什么类型,我们也不知道,它只是用于限定类型的。反编译一下这个 Caculate 类:
    public class Caculate{ public Caculate(){} private Object num; }
    发现编译器擦除 Caculate 类后面的两个尖括号,并且将 num 的类型定义为 Object 类型。
    那么是不是所有的泛型类型都以 Object 进行擦除呢?大部分情况下,泛型类型都会以 Object 进行替换,而有一种情况则不是。那就是使用到了extends和super语法的有界类型,如:
    public class Caculate { private T num; }
    这种情况的泛型类型,num 会被替换为 String 而不再是 Object。这是一个类型限定的语法,它限定 T 是 String 或者 String 的子类,也就是你构建 Caculate 实例的时候只能限定 T 为 String 或者 String 的子类,所以无论你限定 T 为什么类型,String 都是父类,不会出现类型不匹配的问题,于是可以使用 String 进行类型擦除。
    实际上编译器会正常的将使用泛型的地方编译并进行类型擦除,然后返回实例。但是除此之外的是,如果构建泛型实例时使用了泛型语法,那么编译器将标记该实例并关注该实例后续所有方法的调用,每次调用前都进行安全检查,非指定类型的方法都不能调用成功。
    实际上编译器不仅关注一个泛型方法的调用,它还会为某些返回值为限定的泛型类型的方法进行强制类型转换,由于类型擦除,返回值为泛型类型的方法都会擦除成 Object 类型,当这些方法被调用后,编译器会额外插入一行 checkcast 指令用于强制类型转换。这一个过程就叫做『泛型翻译』。

    56. 什么是泛型中的限定通配符和非限定通配符 ?

    限定通配符对类型进行了限制。有两种限定通配符,一种是<? extends T>它通过确保类型必须是T的子类来设定类型的上界,另一种是<? super T>它通过确保类型必须是T的父类来设定类型的下界。泛型类型必须用限定内的类型来进行初始化,否则会导致编译错误。
    非限定通配符 ,可以用任意类型来替代。如List<?> 的意思是这个集合是一个可以持有任意类型的集合,它可以是List,也可以是List,或者List等等。

    57. List<? extends T>和List <? super T>之间有什么区别 ?

    这两个List的声明都是限定通配符的例子,List<? extends T>可以接受任何继承自T的类型的List,而List<? super T>可以接受任何T的父类构成的List。例如List<? extends Number>可以接受List或List。

    58. 可以把List传递给一个接受List参数的方法吗?

    不可以。真这样做的话会导致编译错误。因为List可以存储任何类型的对象包括String, Integer等等,而List却只能用来存储String。
    List objectList; List stringList; objectList = stringList; //compilation error incompatible types

    59. 判断ArrayList与ArrayList是否相等?

    ArrayList a = new ArrayList(); ArrayList b = new ArrayList(); Class c1 = a.getClass(); Class c2 = b.getClass(); System.out.println(c1 == c2);
    输出的结果是 true。因为无论对于 ArrayList 还是 ArrayList,它们的 Class 类型都是一直的,都是 ArrayList.class。
    那它们声明时指定的 String 和 Integer 到底体现在哪里呢?
    答案是体现在类编译的时候。当 JVM 进行类编译时,会进行泛型检查,如果一个集合被声明为 String 类型,那么它往该集合存取数据的时候就会对数据进行判断,从而避免存入或取出错误的数据。
    补充: Array中可以用泛型吗?
    不可以。这也是为什么 Joshua Bloch 在 《Effective Java》一书中建议使用 List 来代替 Array,因为 List 可以提供编译期的类型安全保证,而 Array 却不能。

    60. Java序列化与反序列化是什么?

    Java序列化是指把Java对象转换为字节序列的过程,而Java反序列化是指把字节序列恢复为Java对象的过程:

  • 序列化:序列化是把对象转换成有序字节流,以便在网络上传输或者保存在本地文件中。核心作用是对象状态的保存与重建。我们都知道,Java对象是保存在JVM的堆内存中的,也就是说,如果JVM堆不存在了,那么对象也就跟着消失了。
    而序列化提供了一种方案,可以让你在即使JVM停机的情况下也能把对象保存下来的方案。就像我们平时用的U盘一样。把Java对象序列化成可存储或传输的形式(如二进制流),比如保存在文件中。这样,当再次需要这个对象的时候,从文件中读取出二进制流,再从二进制流中反序列化出对象。

  • 反序列化:客户端从文件中或网络上获得序列化后的对象字节流,根据字节流中所保存的对象状态及描述信息,通过反序列化重建对象。

    61. 为什么需要序列化与反序列化?

    简要描述:对内存中的对象进行持久化或网络传输, 这个时候都需要序列化和反序列化
    深入描述:
    1. 对象序列化可以实现分布式对象。

    主要应用例如:RMI(即远程调用Remote Method Invocation)要利用对象序列化运行远程主机上的服务,就像在本地机上运行对象时一样。

    1. java对象序列化不仅保留一个对象的数据,而且递归保存对象引用的每个对象的数据。

    可以将整个对象层次写入字节流中,可以保存在文件中或在网络连接上传递。利用对象序列化可以进行对象的”深复制”,即复制对象本身及引用的对象本身。序列化一个对象可能得到整个对象序列。

    1. 序列化可以将内存中的类写入文件或数据库中。

    比如:将某个类序列化后存为文件,下次读取时只需将文件中的数据反序列化就可以将原先的类还原到内存中。也可以将类序列化为流数据进行传输。
    总的来说就是将一个已经实例化的类转成文件存储,下次需要实例化的时候只要反序列化即可将类实例化到内存中并保留序列化时类中的所有变量和状态。

    1. 对象、文件、数据,有许多不同的格式,很难统一传输和保存。

    序列化以后就都是字节流了,无论原来是什么东西,都能变成一样的东西,就可以进行通用的格式传输或保存,传输结束以后,要再次使用,就进行反序列化还原,这样对象还是对象,文件还是文件。

    62. 序列化实现的方式有哪些?

    实现Serializable接口或者Externalizable接口。
    Serializable接口
    类通过实现 java.io.Serializable 接口以启用其序列化功能。可序列化类的所有子类型本身都是可序列化的。序列化接口没有方法或字段,仅用于标识可序列化的语义。
    如以下例子:
    import java.io.Serializable; public class User implements Serializable { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } @Override public String toString() { return “User{“ + “name=’” + name + ‘}’; } }
    通过下面的代码进行序列化及反序列化:
    public class SerializableDemo { public static void main(String[] args) { //Initializes The Object User user = new User(); user.setName(“cosen”); System.out.println(user); //Write Obj to File try (FileOutputStream fos = new FileOutputStream(“tempFile”); ObjectOutputStream oos = new ObjectOutputStream( fos)) { oos.writeObject(user); } catch (IOException e) { e.printStackTrace(); } //Read Obj from File File file = new File(“tempFile”); try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))) { User newUser = (User)ois.readObject(); System.out.println(newUser); } catch (IOException | ClassNotFoundException e) { e.printStackTrace(); } } } //OutPut: //User{name=’cosen’} //User{name=’cosen’}
    Externalizable接口
    Externalizable继承自Serializable,该接口中定义了两个抽象方法:writeExternal()与readExternal()。
    当使用Externalizable接口来进行序列化与反序列化的时候需要开发人员重写writeExternal()与readExternal()方法。否则所有变量的值都会变成默认值。
    public class User implements Externalizable { private String name; private int age; public String getName() { return name; } public void setName(String name) { this.name = name; } public void writeExternal(ObjectOutput out) throws IOException { out.writeObject(name); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException { name = (String) in.readObject(); } @Override public String toString() { return “User{“ + “name=’” + name + ‘}’; } }
    通过下面的代码进行序列化及反序列化:
    public class ExternalizableDemo1 { public static void main(String[] args) { //Write Obj to file User user = new User(); user.setName(“cosen”); try(ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(“tempFile”))){ oos.writeObject(user); } catch (IOException e) { e.printStackTrace(); } //Read Obj from file File file = new File(“tempFile”); try(ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file))){ User newInstance = (User) ois.readObject(); //output System.out.println(newInstance); } catch (IOException | ClassNotFoundException e ) { e.printStackTrace(); } } } //OutPut: //User{name=’cosen’}
    两种序列化的对比

    63. 什么是serialVersionUID?

    serialVersionUID 用来表明类的不同版本间的兼容性
    Java的序列化机制是通过在运行时判断类的serialVersionUID来验证版本一致性的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID与本地相应实体(类)的serialVersionUID进行比较,如果相同就认为是一致的,可以进行反序列化,否则就会出现序列化版本不一致的异常。

    64. 为什么还要显示指定serialVersionUID的值?

    如果不显示指定serialVersionUID, JVM在序列化时会根据属性自动生成一个serialVersionUID, 然后与属性一起序列化, 再进行持久化或网络传输. 在反序列化时, JVM会再根据属性自动生成一个新版serialVersionUID, 然后将这个新版serialVersionUID与序列化时生成的旧版serialVersionUID进行比较, 如果相同则反序列化成功, 否则报错.
    如果显示指定了, JVM在序列化和反序列化时仍然都会生成一个serialVersionUID, 但值为我们显示指定的值, 这样在反序列化时新旧版本的serialVersionUID就一致了.
    在实际开发中, 不显示指定serialVersionUID的情况会导致什么问题? 如果我们的类写完后不再修改, 那当然不会有问题, 但这在实际开发中是不可能的, 我们的类会不断迭代, 一旦类被修改了, 那旧对象反序列化就会报错. 所以在实际开发中, 我们都会显示指定一个serialVersionUID, 值是多少无所谓, 只要不变就行。

    65. serialVersionUID什么时候修改?

    《阿里巴巴Java开发手册》中有以下规定:
    image.png
    想要深入了解的小伙伴,可以看这篇文章:https://juejin.cn/post/6844903746682486791

    66. Java 序列化中如果有些字段不想进行序列化,怎么办?

    对于不想进行序列化的变量,使用 transient 关键字修饰。
    transient 关键字的作用是控制变量的序列化,在变量声明前加上该关键字,可以阻止该变量被序列化到文件中,在被反序列化后,transient 变量的值被设为初始值,如 int 型的是 0,对象型的是 null。transient 只能修饰变量,不能修饰类和方法。

    67. 静态变量会被序列化吗?

    不会。因为序列化是针对对象而言的, 而静态变量优先于对象存在, 随着类的加载而加载, 所以不会被序列化.
    看到这个结论, 是不是有人会问, serialVersionUID也被static修饰, 为什么serialVersionUID会被序列化? 其实serialVersionUID属性并没有被序列化, JVM在序列化对象时会自动生成一个serialVersionUID, 然后将我们显示指定的serialVersionUID属性值赋给自动生成的serialVersionUID。

    68. Error 和 Exception 区别是什么?

    Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。Throwable 类有两个重要的子类 Exception(异常)和 Error(错误)。
    Exception 和 Error 二者都是 Java 异常处理的重要子类,各自都包含大量子类。

    • Exception :程序本身可以处理的异常,可以通过 catch 来进行捕获,通常遇到这种错误,应对其进行处理,使应用程序可以继续正常运行。Exception 又可以分为运行时异常(RuntimeException, 又叫非受检查异常)和非运行时异常(又叫受检查异常) 。
    • Error :Error 属于程序无法处理的错误 ,我们没办法通过 catch 来进行捕获 。例如,系统崩溃,内存不足,堆栈溢出等,编译器不会对这类错误进行检测,一旦这类错误发生,通常应用程序会被终止,仅靠应用程序本身无法恢复。

    image.png

    69. 非受检查异常(运行时异常)和受检查异常(一般异常)区别是什么?

    非受检查异常:包括 RuntimeException 类及其子类,表示 JVM 在运行期间可能出现的异常。 Java 编译器不会检查运行时异常。例如:NullPointException(空指针)、NumberFormatException(字符串转换为数字)、IndexOutOfBoundsException(数组越界)、ClassCastException(类转换异常)、ArrayStoreException(数据存储异常,操作数组时类型不一致)等。
    受检查异常:是Exception 中除 RuntimeException 及其子类之外的异常。 Java 编译器会检查受检查异常。常见的受检查异常有: IO 相关的异常、ClassNotFoundException 、SQLException等。
    非受检查异常和受检查异常之间的区别:是否强制要求调用者必须处理此异常,如果强制要求调用者必须进行处理,那么就使用受检查异常,否则就选择非受检查异常。

    70. throw 和 throws 的区别是什么?

    Java 中的异常处理除了包括捕获异常和处理异常之外,还包括声明异常和拋出异常,可以通过 throws 关键字在方法上声明该方法要拋出的异常,或者在方法内部通过 throw 拋出异常对象。
    throws 关键字和 throw 关键字在使用上的几点区别如下:

    • throw 关键字用在方法内部,只能用于抛出一种异常,用来抛出方法或代码块中的异常,受查异常和非受查异常都可以被抛出。
    • throws 关键字用在方法声明上,可以抛出多个异常,用来标识该方法可能抛出的异常列表。一个方法用 throws 标识了可能抛出的异常列表,调用该方法的方法中必须包含可处理异常的代码,否则也要在方法签名中用 throws 关键字声明相应的异常。

    举例如下:
    throw 关键字
    public static void main(String[] args) { String s = “abc”; if(s.equals(“abc”)) { throw new NumberFormatException(); } else { System.out.println(s); } //function(); }
    throws 关键字
    public static void function() throws NumberFormatException{ String s = “abc”; System.out.println(Double.parseDouble(s)); } public static void main(String[] args) { try { function(); } catch (NumberFormatException e) { System.err.println(“非数据类型不能转换。”); //e.printStackTrace(); } }

    71. NoClassDefFoundError 和 ClassNotFoundException 区别?

    NoClassDefFoundError 是一个 Error 类型的异常,是由 JVM 引起的,不应该尝试捕获这个异常。引起该异常的原因是 JVM 或 ClassLoader 尝试加载某类时在内存中找不到该类的定义,该动作发生在运行期间,即编译时该类存在,但是在运行时却找不到了,可能是变异后被删除了等原因导致。
    ClassNotFoundException 是一个受检查异常,需要显式地使用 try-catch 对其进行捕获和处理,或在方法签名中用 throws 关键字进行声明。当使用 Class.forName, ClassLoader.loadClass 或 ClassLoader.findSystemClass 动态加载类到内存的时候,通过传入的类路径参数没有找到该类,就会抛出该异常;另一种抛出该异常的可能原因是某个类已经由一个类加载器加载至内存中,另一个加载器又尝试去加载它。

    72. Java常见异常有哪些?

    • java.lang.IllegalAccessError:违法访问错误。当一个应用试图访问、修改某个类的域(Field)或者调用其方法,但是又违反域或方法的可见性声明,则抛出该异常。
    • java.lang.InstantiationError:实例化错误。当一个应用试图通过Java的new操作符构造一个抽象类或者接口时抛出该异常.
    • java.lang.OutOfMemoryError:内存不足错误。当可用内存不足以让Java虚拟机分配给一个对象时抛出该错误。
    • java.lang.StackOverflowError:堆栈溢出错误。当一个应用递归调用的层次太深而导致堆栈溢出或者陷入死循环时抛出该错误。
    • java.lang.ClassCastException:类造型异常。假设有类A和B(A不是B的父类或子类),O是A的实例,那么当强制将O构造为类B的实例时抛出该异常。该异常经常被称为强制类型转换异常。
    • java.lang.ClassNotFoundException:找不到类异常。当应用试图根据字符串形式的类名构造类,而在遍历CLASSPAH之后找不到对应名称的class文件时,抛出该异常。
    • java.lang.ArithmeticException:算术条件异常。譬如:整数除零等。
    • java.lang.ArrayIndexOutOfBoundsException:数组索引越界异常。当对数组的索引值为负数或大于等于数组大小时抛出。
    • java.lang.IndexOutOfBoundsException:索引越界异常。当访问某个序列的索引值小于0或大于等于序列大小时,抛出该异常。
    • java.lang.InstantiationException:实例化异常。当试图通过newInstance()方法创建某个类的实例,而该类是一个抽象类或接口时,抛出该异常。
    • java.lang.NoSuchFieldException:属性不存在异常。当访问某个类的不存在的属性时抛出该异常。
    • java.lang.NoSuchMethodException:方法不存在异常。当访问某个类的不存在的方法时抛出该异常。
    • java.lang.NullPointerException:空指针异常。当应用试图在要求使用对象的地方使用了null时,抛出该异常。譬如:调用null对象的实例方法、访问null对象的属性、计算null对象的长度、使用throw语句抛出null等等。
    • java.lang.NumberFormatException:数字格式异常。当试图将一个String转换为指定的数字类型,而该字符串确不满足数字类型要求的格式时,抛出该异常。
    • java.lang.StringIndexOutOfBoundsException:字符串索引越界异常。当使用索引值访问某个字符串中的字符,而该索引值小于0或大于等于序列大小时,抛出该异常。

      73. try-catch-finally 中哪个部分可以省略?

      catch 可以省略。更为严格的说法其实是:try只适合处理运行时异常,try+catch适合处理运行时异常+普通异常。也就是说,如果你只用try去处理普通异常却不加以catch处理,编译是通不过的,因为编译器硬性规定,普通异常如果选择捕获,则必须用catch显示声明以便进一步处理。而运行时异常在编译时没有如此规定,所以catch可以省略,你加上catch编译器也觉得无可厚非。
      理论上,编译器看任何代码都不顺眼,都觉得可能有潜在的问题,所以你即使对所有代码加上try,代码在运行期时也只不过是在正常运行的基础上加一层皮。但是你一旦对一段代码加上try,就等于显示地承诺编译器,对这段代码可能抛出的异常进行捕获而非向上抛出处理。如果是普通异常,编译器要求必须用catch捕获以便进一步处理;如果运行时异常,捕获然后丢弃并且+finally扫尾处理,或者加上catch捕获以便进一步处理。
      至于加上finally,则是在不管有没捕获异常,都要进行的“扫尾”处理。

      74. try-catch-finally 中,如果 catch 中 return 了,finally 还会执行吗?

      会执行,在 return 前执行。
      在 finally 中改变返回值的做法是不好的,因为如果存在 finally 代码块,try中的 return 语句不会立马返回调用者,而是记录下返回值待 finally 代码块执行完毕之后再向调用者返回其值,然后如果在 finally 中修改了返回值,就会返回修改后的值。显然,在 finally 中返回或者修改返回值会对程序造成很大的困扰,Java 中也可以通过提升编译器的语法检查级别来产生警告或错误。代码示例1:
      public static int getInt() { int a = 10; try { System.out.println(a / 0); a = 20; } catch (ArithmeticException e) { a = 30; return a; / return a 在程序执行到这一步的时候,这里不是return a 而是 return 30;这个返回路径就形成了 但是呢,它发现后面还有finally,所以继续执行finally的内容,a=40 再次回到以前的路径,继续走return 30,形成返回路径之后,这里的a就不是a变量了,而是常量30 / } finally { a = 40; } return a; } //执行结果:30
      *代码示例2:

      public static int getInt() { int a = 10; try { System.out.println(a / 0); a = 20; } catch (ArithmeticException e) { a = 30; return a; } finally { a = 40; //如果这样,就又重新形成了一条返回路径,由于只能通过1个return返回,所以这里直接返回40 return a; } } // 执行结果:40

      75. JVM 是如何处理异常的?

      在一个方法中如果发生异常,这个方法会创建一个异常对象,并转交给 JVM,该异常对象包含异常名称,异常描述以及异常发生时应用程序的状态。创建异常对象并转交给 JVM 的过程称为抛出异常。可能有一系列的方法调用,最终才进入抛出异常的方法,这一系列方法调用的有序列表叫做调用栈。
      JVM 会顺着调用栈去查找看是否有可以处理异常的代码,如果有,则调用异常处理代码。当 JVM 发现可以处理异常的代码时,会把发生的异常传递给它。如果 JVM 没有找到可以处理该异常的代码块,JVM 就会将该异常转交给默认的异常处理器(默认处理器为 JVM 的一部分),默认异常处理器打印出异常信息并终止应用程序。
      想要深入了解的小伙伴可以看这篇文章:https://www.cnblogs.com/qdhxhz/p/10765839.html

      76. Java的IO 流分为几种?

    • 按照流的方向:输入流(inputStream)和输出流(outputStream);

    • 按照实现功能分:节点流(可以从或向一个特定的地方读写数据,如 FileReader)和处理流(是对一个已存在的流的连接和封装,通过所封装的流的功能调用实现数据读写, BufferedReader);
    • 按照处理数据的单位: 字节流和字符流。分别由四个抽象类来表示(每种流包括输入和输出两种所以一共四个):InputStream,OutputStream,Reader,Writer。Java中其他多种多样变化的流均是由它们派生出来的。

    image.png

    77. 字节流如何转为字符流?

    字节输入流转字符输入流通过 InputStreamReader 实现,该类的构造函数可以传入 InputStream 对象。
    字节输出流转字符输出流通过 OutputStreamWriter 实现,该类的构造函数可以传入 OutputStream 对象。

    78. 字符流与字节流的区别?

    • 读写的时候字节流是按字节读写,字符流按字符读写。
    • 字节流适合所有类型文件的数据传输,因为计算机字节(Byte)是电脑中表示信息含义的最小单位。字符流只能够处理纯文本数据,其他类型数据不行,但是字符流处理文本要比字节流处理文本要方便。
    • 在读写文件需要对内容按行处理,比如比较特定字符,处理某一行数据的时候一般会选择字符流。
    • 只是读写文件,和文件内容无关时,一般选择字节流。

      79. 什么是阻塞IO?什么是非阻塞IO?

      IO操作包括:对硬盘的读写、对socket的读写以及外设的读写。
      当用户线程发起一个IO请求操作(本文以读请求操作为例),内核会去查看要读取的数据是否就绪,对于阻塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪;对于非阻塞IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数据没有就绪。当数据就绪之后,便将数据拷贝到用户线程,这样才完成了一个完整的IO读请求操作,也就是说一个完整的IO读请求操作包括两个阶段:
      1)查看数据是否就绪;
      2)进行数据拷贝(内核将数据拷贝到用户线程)。
      那么阻塞(blocking IO)和非阻塞(non-blocking IO)的区别就在于第一个阶段,如果数据没有就绪,在查看数据是否就绪的过程中是一直等待,还是直接返回一个标志信息。
      Java中传统的IO都是阻塞IO,比如通过socket来读数据,调用read()方法之后,如果数据没有就绪,当前线程就会一直阻塞在read方法调用那里,直到有数据才返回;而如果是非阻塞IO的话,当数据没有就绪,read()方法应该返回一个标志信息,告知当前线程数据没有就绪,而不是一直在那里等待。
      深入了解可看这篇文章:https://mp.weixin.qq.com/s/p5qM2UJ1uIWyongfVpRbCg

      80. BIO、NIO、AIO的区别?

    • BIO:同步并阻塞,在服务器中实现的模式为一个连接一个线程。也就是说,客户端有连接请求的时候,服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然这也可以通过线程池机制改善。BIO一般适用于连接数目小且固定的架构,这种方式对于服务器资源要求比较高,而且并发局限于应用中,是JDK1.4之前的唯一选择,但好在程序直观简单,易理解。

    • NIO:同步并非阻塞,在服务器中实现的模式为一个请求一个线程,也就是说,客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到有连接IO请求时才会启动一个线程进行处理。NIO一般适用于连接数目多且连接比较短(轻操作)的架构,并发局限于应用中,编程比较复杂,从JDK1.4开始支持。
    • AIO:异步并非阻塞,在服务器中实现的模式为一个有效请求一个线程,也就是说,客户端的IO请求都是通过操作系统先完成之后,再通知服务器应用去启动线程进行处理。AIO一般适用于连接数目多且连接比较长(重操作)的架构,充分调用操作系统参与并发操作,编程比较复杂,从JDK1.7开始支持。

      81. Java IO都有哪些设计模式?

      使用了适配器模式装饰器模式
      适配器模式
      Reader reader = new INputStreamReader(inputStream);
      把一个类的接口变换成客户端所期待的另一种接口,从而使原本因接口不匹配而无法在一起工作的两个类能够在一起工作

    • 类适配器:Adapter类(适配器)继承Adaptee类(源角色)实现Target接口(目标角色)

    • 对象适配器:Adapter类(适配器)持有Adaptee类(源角色)对象实例,实现Target接口(目标角色)

    image.png

    装饰器模式
    new BufferedInputStream(new FileInputStream(inputStream));
    一种动态地往一个类中添加新的行为的设计模式。就功能而言,装饰器模式相比生成子类更为灵活,这样可以给某个对象而不是整个类添加一些功能。

    • ConcreteComponent(具体对象)和Decorator(抽象装饰器)实现相同的Conponent(接口)并且Decorator(抽象装饰器)里面持有Conponent(接口)对象,可以传递请求。
    • ConcreteComponent(具体装饰器)覆盖Decorator(抽象装饰器)的方法并用super进行调用,传递请求。

    image.png

    82. Files的常用方法都有哪些?

    • Files.exists():检测文件路径是否存在。
    • Files.createFile():创建文件。
    • Files.createDirectory():创建文件夹。
    • Files.delete():删除一个文件或目录。
    • Files.copy():复制文件。
    • Files.move():移动文件。
    • Files.size():查看文件个数。
    • Files.read():读取文件。
    • Files.write():写入文件。