微信公众号:放开我我还能学
分享知识,共同进步!
- 什么是 JVM?
- 什么是字节码?采用字节码的好处是什么?
- Java 程序执行步骤分为哪几步?
- Java 是编译型还是解释型语言?
- JDK 和 JRE 的区别
- switch 语句可以用于哪些数据类型
- Java 面向对象编程三大特性
- 字符型常量和字符串常量的区别?
- String 是什么,能否被继承?
- 创建 String 对象有哪些方式?
- new String("abc") 做了哪些事情?
- 为什么 String 要设计成不可变?
- 为什么我们在使用 HashMap 的时候总是用 String 做 key
- String,StringBuffer,StringBuilder 的区别
- == 与 equals 的区别
- 为什么重写 equals 时必须重写 hashCode 方法?
- 接口和抽象类的区别
- this 和 super
- 说说对 final 关键字的理解
- 局部变量和成员变量的区别
- 静态变量和实例变量的区别
- Java 是值传递还是引用传递?
- 方法参数需要被内部类使用时,这个参数必须被 final 修饰的原因
- Java 序列化中如果指定某些字段不进行序列化?
- 重载和重写的区别
- 方法重载与哪些因素有关?
- 说说 Java 的自动类型转换(隐式类型转换)
- 说说 Java 初始化顺序
- 谈谈你对内部类的理解?
- Java 中的异常处理
- 深拷贝和浅拷贝
- 在 java.lang.Object 中对 clone() 方法的约定有哪些?
- 实现深拷贝的方法有哪些?
- 整型包装类值的比较可以用 == 吗?
- new Integer() 与 Integer.valueOf() 的区别?
- 浮点数如何做等值判断?
- 什么是动态代理,它和静态代理的区别?
- JDK 和 CGLib 动态代理的区别
- 什么是反射?什么是字节码增强?
- 什么是哈希算法
什么是 JVM?
Java 虚拟机(JVM)是运行 Java 字节码的虚拟机。JVM 有针对不同系统的特定实现(Windows,Linux,macOS),目的是使用相同的字节码,它们都会给出相同的结果。
什么是字节码?采用字节码的好处是什么?
在 Java 中,JVM 可以理解的代码就叫做字节码(即扩展名为 .class 的文件),它不面向任何特定的处理器,只面向虚拟机。
Java 语言通过字节码的方式,在一定程度上解决了传统解释型语言执行效率低的问题,同时又保留了解释型语言可移植的特点。所以 Java 程序运行时比较高效,而且,由于字节码并不针对一种特定的机器,因此,Java 程序无须重新编译便可在多种不同操作系统的计算机上运行。
Java 程序执行步骤分为哪几步?
步骤为:.java 文件 -> .class 文件 -> 二进制机器码
Java 是编译型还是解释型语言?
Java 既是编译型语言,又是解释型语言。
说它是编译型语言,因为所有的 Java 代码都是要经过编译的。
说他是解释型语言,因为 Java 代码编译后不能直接运行,需要由 JVM 解释运行。
当然,现在的主流 JVM 使用解释器与编译器(JIT)并存的架构。解释器与编译器两者各有优势:当程序需要迅速启动和执行时,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大,可以使用解释器执行节约内存,反之可以使用编译执行来提升效率。
JDK 和 JRE 的区别
JDK 是 Java Development Kit,它是功能齐全的 Java SDK。它拥有 JRE 所拥有的一切,还有编译器(javac)和工具(如 javadoc 和 jdb)。它能够创建和编译程序。
JRE 是 Java 运行时环境。它是运行已编译 Java 程序所需的所有内容的集合,包括 Java 虚拟机(JVM),Java 类库,java 命令和其他的一些基础构件。但是,它不能用于创建新程序。
switch 语句可以用于哪些数据类型
switch 语句可以用于 char, byte, short, int 这些数据基本类型,以及它们的包装类。
在 switch 里不能用 long, double, float, boolean 包括它们的包装类。
jdk1.7 以上可以用字符串类型。
switch 中可以用枚举类型。
Java 面向对象编程三大特性
封装
封装把一个对象的属性私有化,同时提供一些可以被外界访问的属性的方法,如果属性不想被外界访问,我们大可不必提供方法给外界访问。但是如果一个类没有提供给外界访问的方法,那么这个类也没有什么意义了。
继承
继承是使用已存在的类的定义作为基础建立新类的技术,新类的定义可以增加新的数据或新的功能,也可以用父类的功能。通过使用继承我们能够非常方便地复用以前的代码。
- 子类拥有父类对象所有的属性和方法(包括私有属性和私有方法),但是父类中的私有属性和方法子类是无法访问,只是拥有。
- 子类可以拥有自己属性和方法,即子类可以对父类进行扩展。
- 子类可以用自己的方式实现父类的方法。
多态
多态是指程序中定义的引用变量所指向的具体类型和通过该引用变量发出的方法调用在编程时并不确定,而是在程序运行期间才确定,即一个引用变量到底会指向哪个类的实例对象,该引用变量发出的方法调用到底是哪个类中实现的方法,必须在由程序运行期间才能决定。
在 Java 中有两种形式可以实现多态:继承(多个子类对同一方法的重写)和接口(实现接口并覆盖接口中同一方法)。
字符型常量和字符串常量的区别?
字符常量是单引号引起的一个字符,字符串常量是双引号引起的若干个字符。
字符常量相当于一个整型值( ASCII 值),可以参加表达式运算,字符串常量代表一个地址值(该字符串在内存中存放位置)。
字符常量只占 2 个字节,字符串常量占若干个字节(注意: char 在 Java 中占两个字节)
String 是什么,能否被继承?
String 是定义在 java.lang 包下的一个类,它不是基本数据类型。
String 是不可变的,JVM 使用字符串池来存储所有的字符串对象。
String 不能被继承,因为被 final 修饰的类不能被继承。
创建 String 对象有哪些方式?
1、使用 new 关键字创建
使用这种方式时,JVM 创建字符串对象但不存储于字符串池。可以通过调用 intern() 方法将该字符串对象存储在字符串池,如果字符串池已经有了同样值的字符串,则返回引用。
使用 new 关键字创建有如下三种方式:
-
new String()
-
new String(char[ ] array)
-
new String(byte[ ] array)
2、使用双引号创建
使用这种方式时,JVM 去字符串池找有没有值相等字符串,如果有,则返回找到的字符串引用,否则创建一个新的字符串对象并存储在字符串池。
示例:
String s1 = new String("123");
String s2 = "123";
System.out.println(s1 == s2); // false
String s1 = new String("123").intern();
String s2 = "123";
System.out.println(s1 == s2); // true
new String("abc") 做了哪些事情?
使用这种方式一共会创建两个字符串对象(前提是 String Pool 中还没有 "abc" 字符串对象)
- "abc" 属于字符串字面量,因此编译时期会在 String Pool 中创建一个字符串对象,指向这个 "abc" 字符串字面量;
- 而使用 new 的方式会在堆中创建一个字符串对象。
为什么 String 要设计成不可变?
可以缓存 hash 值
因为 String 的 hash 值经常被使用,例如 String 用做 HashMap 的 key。不可变的特性可以使得 hash 值也不可变,因此只需要进行一次计算。
便于实现 String 常量池
只有当字符串是不可变的,字符串池才有可能实现。字符串池的实现可以在运行时节约堆空间,因为不同的字符串变量都指向池中的同一个字符串。但如果字符串是可变的,那么 String interning(字符串驻留) 将不能实现。(字符串驻留是指对不同的字符串仅仅只保存一个,即不会保存多个相同的字符串)
使多线程安全
因为字符串是不可变的,所以是多线程安全的,同一个字符串实例可以被多个线程共享。这样便不用因为线程安全问题而使用同步机制,字符串自己便是线程安全的。
避免网络安全问题
如果字符串是可变的,那么会引起很严重的安全问题。譬如,数据库的用户名、密码都是以字符串的形式传入来获得数据库的连接,或者在 socket 编程中,主机名和端口都是以字符串的形式传入。因为字符串是不可变的,所以它的值是不可改变的,否则黑客们可以钻到空子,改变字符串指向的对象的值,造成安全漏洞。
加快字符串处理速度
因为字符串是不可变的,所以在它创建的时候 hashCode 就被缓存了,不需要重新计算。这就使得字符串很适合作为 Map 中的键,字符串的处理速度要快过其它的键对象,因此 HashMap 中的键往往都使用字符串。
为什么我们在使用 HashMap 的时候总是用 String 做 key
我们来看下 String#hashCode 的源码:
public final class String {
// 默认值是0
private int hash;
public int hashCode() {
// 将成员变量hash缓存到局部变量
int h = hash;
// 这里使用的是局部变量,没有多线程修改的风险
// 如果之前没有缓存过,那么h为0,计算hash值并进行缓存
if (h == 0 && value.length > 0) {
char val[] = value;
for (int i = 0; i < value.length; i++) {
h = 31 * h + val[i];
}
hash = h;
}
// 如果之前缓存过,直接返回hash值
return h;
}
}
由此可见,因为字符串是不可变的,当创建字符串时,它的 hashCode 被缓存下来,不需要再次计算。因为 HashMap 内部实现是通过 key 的 hashCode 来确定 value 的存储位置,所以相比于其他对象更快。
String,StringBuffer,StringBuilder 的区别
String 是不可变类,每当我们对 String 进行操作的时候,总是会创建新的字符串,操作 String 很耗资源,所以 Java 提供了两个工具类 StringBuffer 和 StringBuilder。
StringBuffer 和 StringBuilder 是可变类,StringBuffer 是线程安全的,StringBuilder 则不是线程安全的。所以在多线程对同一个字符串操作的时候,我们应该选择用 StringBuffer。在单线程的情况下,StringBuilder 的效率比 StringBuffer 高。
== 与 equals 的区别
== :它的作用是判断两个对象的地址是否相等。即判断两个对象是不是同一个对象(基本数据类型 == 比较的是值,引用数据类型 == 比较的是内存地址)。
equals():它的作用也是判断两个对象是否相等,但它一般有两种使用情况:
- 类没有重写
equals()方法,则等价于通过==比较这两个对象。 - 类重写了
equals()方法,则比较两个对象的内容是否相等。
示例:
public static void main(String[] args) {
String s1 = new String("abc"); // s1 为一个引用
String s2 = new String("abc"); // s2 为另一个引用,对象的内容一样
String s3 = "abc"; // 放在常量池中
String s4 = "abc"; // 从常量池中获取
if (s3 == s4) // true
System.out.println("s3==s4");
if (s1 == s2) // false,非同一对象
System.out.println("s1==s2");
if (s1.equals(s2)) // true
System.out.println("s1.equals(s2)");
if (42 == 42.0) { // true
System.out.println("true");
}
}
String#equals 是被重写过的,因为 Object#equals 是比较的对象的内存地址,而 String#equals 比较的是对象的值。
当创建 String 类型的对象时,虚拟机会在常量池中查找有没有已经存在的值和要创建的值相同的对象,如果有就把它赋给当前引用,没有就在常量池中重新创建一个 String 对象。
为什么重写 equals 时必须重写 hashCode 方法?
为了提高效率。首先我们知道 hashCode 方法返回的是对象存储的物理地址的一个索引,那么每当添加新元素的时候,先通过索引查看这个位置是否存在元素,如果不存在则可以直接将元素存储于此,不需要再调用 equals 方法;如果已经存在元素(哈希冲突),则再调用 equals 方法与新元素进行比较,相同的话就不存了,不相同的话就在该索引处用链表或红黑树形式向后存储。这样就使实际调用 equals 方法的次数大大降低了,提高了运算效率。
因此我们也不难理解如下两句话:
如果两个对象相同,那么它们的 hashCode 值一定相同。
如果两个对象的 hashCode 值相同,它们并不一定相同。
接口和抽象类的区别
抽象类的特点:
- 抽象方法只作声明,而不包含实现,可以看成是没有实现体的虚方法
- 抽象类不能被实例化
- 抽象类可以但不是必须有抽象属性和抽象方法,但是一旦有了抽象方法,就一定要把这个类声明为抽象类
- 具体派生类必须覆盖基类的抽象方法
- 抽象派生类可以覆盖基类的抽象方法,也可以不覆盖。如果不覆盖,则其具体派生类必须覆盖它们
- 抽象方法不能用 private, static 修饰
接口的特点:
-
接口不能被实例化
-
接口只能包含方法声明
-
接口中成员变量默认修饰符是 public static final,方法的默认修饰符是 public abstract
-
接口中可以包含的内容:
Java 7:常量(接口中的成员变量用 public static final修饰,但是可以省略)、抽象方法(接口中的抽象方法前缀 public abstract 可以省略)
Java 8:新增默认方法(接口中的默认方法解决了接口升级的问题)、静态方法
注意:多个父接口中的默认方法如果重复,那么子接口必须进行默认方法的重写(带着 default 关键字)
Java 9:新增私有方法(接口中私有方法的出现可以解决接口中多个默认方法或静态方法之间出现重复代码的问题,分为普通私有方法和静态私有方法)
接口和抽象类的区别:
- 抽象类可以有构造方法,接口中不能有构造方法
- 抽象类中可以有普通成员变量,接口中没有普通成员变量
- 抽象类中可以包含静态方法,接口中不能包含静态方法(Java8 之前)
- 抽象类是对类的抽象,对逻辑的归纳,比如人类是中国人和美国人的抽象。接口是对功能或行为的抽象
this 和 super
this 关键字的三种用法:
- 在本类的成员方法中,访问本类的成员变量
- 在本类的成员方法中,访问本类的另一个成员方法
- 在本类的构造方法中,访问本类的另一个构造方法
super 关键字的三种用法:
- 在子类的成员方法中,访问父类的成员变量
- 在子类的成员方法中,访问父类的成员方法
- 在子类的构造方法中,访问父类的构造方法
说说对 final 关键字的理解
final 修饰类,此类不能继承。
final 修饰方法,方法不能重写。
final 修饰局部变量,基本类型值无法改变,引用类型指向地址值无法改变。
final 修饰成员变量,需要手动赋值或构造方法赋值。
局部变量和成员变量的区别
1、定义的位置不一样
局部变量:在方法的内部
成员变量:在方法的外部
2、作用范围不一样
局部变量:只有在方法中可以使用
成员变量:整个类都可以使用
3、默认值不一样
局部变量:没有默认值
成员变量:有默认值
4、内存的位置不一样
局部变量:位于栈中
成员变量:位于堆中
5、生命周期不一样
局部变量:随着方法入栈而产生,随着方法出栈而销毁
成员变量:随着对象创建而产生,随着对象被垃圾回收而销毁
静态变量和实例变量的区别
语法定义角度:
静态变量前要加 static 关键字,而实例变量前则不加
程序运行角度:
静态变量则可以直接使用类名来引用。静态变量不属于某个实例对象,而是属于类,所以也称为类变量,只要程序加载了类的字节码,不用创建任何实例对象,静态变量就会被分配空间,静态变量就可以被使用了。且一个类不管创建多少个对象,静态变量在内存中有且仅有一个拷贝。
实例变量属于某个对象的属性,必须创建了实例对象,其中的实例变量才会被分配空间,才能使用这个实例变量。
Java 是值传递还是引用传递?
值传递:在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时形参接收到的内容是实参值的一个拷贝,因此在方法内对形参的任何操作,都仅仅是对这个副本的操作,不影响原始值的内容。值传递传递的是真实内容的一个副本,对副本的操作不影响原内容,也就是形参怎么变化,不会影响实参对应的内容。
引用传递:引用也就是指向真实内容的地址值,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,形参和实参指向同一块内存地址,对形参的操作会影响的真实内容。
在 Java 中只有值传递
请看如下示例:
public static void main(String[] args) {
Student s1 = new Student("John");
Student s2 = new Student("Mike");
swap(s1, s2);
System.out.println("s1:" + s1.getName());
System.out.println("s2:" + s2.getName());
}
public static void swap(Student x, Student y) {
Student temp = x;
x = y;
y = temp;
System.out.println("x:" + x.getName());
System.out.println("y:" + y.getName());
}
结果输出:
x:Mike
y:John
s1:John
s2:Mike
由此可以看出, 方法并没有改变存储在变量 s1 和 s2 中的对象引用。swap 方法的参数 x 和 y 被初始化为两个对象引用的拷贝,交换的是这两个拷贝,原来的引用不受影响。
方法参数需要被内部类使用时,这个参数必须被 final 修饰的原因
外部类和内部类是平行的,内部类不从属于外部类,因此外部类有可能在内部类之前被回收。如果不加 final,一旦外部类在内部类之前被回收,那么外部类中所包含的对象也会被回收,这时内部类还未使用该对象,一旦使用就会报 NPE。
如果在此参数前加上 final,那么这个参数就是常量了,存储位置就由堆区转移到常量池中,从而不会被 Minor GC。
Java 序列化中如果指定某些字段不进行序列化?
对于不想进行序列化的变量,使用 transient 关键字修饰。
transient 关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient 修饰的变量值不会被持久化和恢复。transient 只能修饰变量,不能修饰类和方法。
重载和重写的区别
重载就是同样的一个方法能够根据输入数据的不同,做出不同的处理。
重写就是当子类继承自父类的相同方法,输入数据一样,但要做出有别于父类的响应时,你就要覆盖父类方法。
方法重载与哪些因素有关?
与参数个数、参数类型、参数顺序有关。
注意:与方法的返回值无关。
说说 Java 的自动类型转换(隐式类型转换)
数据范围从小到大,小的可以赋值给大的。
示例:
short s1 = 1;
s1 = s1 + 1; // 错误,因为字面量1是int类型,它比short类型精度要高,因此不能隐式地将int类型向下转型为short类型
// 但是使用 += 或者 ++ 运算符会执行隐式类型转换
s1 += 1;
s1++;
// 上面的语句相当于将s1 + 1的计算结果进行了向下转型
s1 = (short) (s1 + 1);
另外,float 虽然只占 4 字节,但是比 8 字节的 long 范围更大。byte 和 short 类型运算时,会提升为 int 的运算。
说说 Java 初始化顺序
普通类:
- 静态变量
- 静态代码块
- 普通变量
- 普通代码块
- 构造函数
继承的子类:
- 父类静态变量
- 父类静态代码块
- 子类静态变量
- 子类静态代码块
- 父类普通变量
- 父类普通代码块
- 父类构造函数
- 子类普通变量
- 子类普通代码块
- 子类构造函数
谈谈你对内部类的理解?
Java 有四种内部类:
1、成员内部类
直接创建的内部类。
2、局部内部类
在方法体或语句块(包括方法、构造方法、局部块或静态初始化块)内部定义的类成为局部内部类。局部内部类不能加任何访问修饰符,因为它只对局部块有效。
在局部内部类中可以使用外部类的局部变量,但是这个局部变量必须加 final(JDK1.8 之前)。因为局部内部类的对象可以被返回到外部类的外面进行使用,如果不是 final 的,这个局部变量在方法结束后就消失了,那么再通过局部内部类的对象使用这个变量就有问题了。
3、匿名内部类
如果某个类的实例只使用一次,则可以将类的定义与类的创建一起完成,或者说在定义类的同时就创建一个类。以这种方法定义的没有名字的类称为匿名内部类。
匿名内部类是一个接口的实现类或抽象类的子类的对象。
4、静态内部类
使用 static 关键字创建的内部类。
使用示例:
public class Outer {
private int num;
// 成员内部类
public class Inner {
}
private static int num2;
// 静态内部类
public static class StaticInner {
}
public void method() {
// jdk1.8之前必须是final类型
int num = 5;
// 局部内部类
class LocalInner {
void print() {
System.out.println("num = " + num);
}
}
// 局部内部类只能在局部范围使用
LocalInner localInner = new LocalInner();
localInner.print();
}
public static void main(String[] args) {
new Outer().method();
// 成员内部类构造对象
Outer.Inner inner = new Outer().new Inner();
// 静态内部类构造对象
Outer.StaticInner staticInner = new Outer.StaticInner();
// 匿名内部类构造对象
A a = new A() {
@Override
public void method() {
System.out.println("a");
}
};
B b = new B() {
@Override
void method() {
System.out.println("b");
}
};
}
}
interface A {
void method();
}
abstract class B {
abstract void method();
}
Java 中的异常处理
在 Java 中,所有的异常都有一个共同的祖先 java.lang 包中的 Throwable 类。
Throwable 类有两个重要的子类:Exception 和 Error 。
Error 是程序无法处理的错误,比如 OutOfMemoryError、ThreadDeath 等。这些异常发生时,JVM 一般会选择线程终止。
Exception 是程序本身可以处理的异常,这种异常又分为两类:
- 运行时异常(非受检查异常)
运行时异常都是 RuntimeException 类及其子类,如 NullPointerException、IndexOutOfBoundsException 等,这些异常是不受检查异常,程序中可以选择捕获处理,也可以不处理。这些异常一般是由程序逻辑错误引起的,程序应该从逻辑角度尽可能避免这类异常的发生。
- 非运行时异常(受检查异常)
非运行时异常是 RuntimeException 以外的异常,类型上都属于 Exception 类及其子类。从程序语法角度讲是必须进行处理的异常,如果不处理,程序就不能编译通过。如 IOException、SQLException 等以及用户自定义的 Exception 异常。
深拷贝和浅拷贝
浅拷贝:拷贝对象和原始对象的引用类型引用同一个对象。
如果原型对象的成员变量是值类型,则将复制一份给克隆对象;如果原型对象的成员变量是引用类型,则将引用对象的地址复制一份给克隆对象,也就是说,原型对象和克隆对象的成员变量指向相同的内存地址。在浅拷贝中,当对象被复制时只复制它本身和其中包含的值类型,而引用类型的成员变量并没有复制。
public class ShallowCloneExample implements Cloneable {
private int[] arr;
public ShallowCloneExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
public void set(int index, int value) {
arr[index] = value;
}
public int get(int index) {
return arr[index];
}
@Override
protected ShallowCloneExample clone() throws CloneNotSupportedException {
return (ShallowCloneExample) super.clone();
}
}
ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = null;
try {
e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
// e1的改变会反映到e2
e1.set(2, 222);
System.out.println(e2.get(2)); // 222
深拷贝:拷贝对象和原始对象的引用类型引用不同对象。
在深拷贝中,无论原型对象的成员变量是值类型还是引用类型,都将复制一份给克隆对象。
public class DeepCloneExample implements Cloneable {
private int[] arr;
public DeepCloneExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
public void set(int index, int value) {
arr[index] = value;
}
public int get(int index) {
return arr[index];
}
@Override
protected DeepCloneExample clone() throws CloneNotSupportedException {
DeepCloneExample result = (DeepCloneExample) super.clone();
result.arr = new int[arr.length];
for (int i = 0; i < arr.length; i++) {
result.arr[i] = arr[i];
}
return result;
}
}
DeepCloneExample e1 = new DeepCloneExample();
DeepCloneExample e2 = null;
try {
e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
// e1的改变不会反映到e2
e1.set(2, 222);
System.out.println(e2.get(2)); // 2
深拷贝的替代方案
public class CloneConstructorExample {
private int[] arr;
public CloneConstructorExample() {
arr = new int[10];
for (int i = 0; i < arr.length; i++) {
arr[i] = i;
}
}
public CloneConstructorExample(CloneConstructorExample original) {
arr = new int[original.arr.length];
for (int i = 0; i < original.arr.length; i++) {
arr[i] = original.arr[i];
}
}
public void set(int index, int value) {
arr[index] = value;
}
public int get(int index) {
return arr[index];
}
}
CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1);
e1.set(2, 222);
System.out.println(e2.get(2)); // 2
在 java.lang.Object 中对 clone() 方法的约定有哪些?
从源码的注释信息中我们可以看出,Object 对 clone() 方法的约定有三条:
- 对于所有对象来说,
x.clone() != x应当返回 true,因为克隆对象与原对象不是同一个对象; - 对于所有对象来说,
x.clone().getClass() == x.getClass()应当返回 true,因为克隆对象与原对象的类型是一样的; - 对于所有对象来说,
x.clone().equals(x)应当返回 true,因为使用 equals 比较时,它们的值都是相同的。
除了注释信息外,我们看 clone() 的实现方法,发现 clone() 是使用 native 修饰的本地方法,因此执行的性能会很高,并且它返回的类型为 Object,因此在调用克隆之后要把对象强转为目标类型才行。
实现深拷贝的方法有哪些?
深克隆的实现方式有很多种,大体可以分为以下几类:
-
所有对象都实现克隆方法;
-
通过构造方法实现深克隆;
-
使用 JDK 自带的字节流实现深克隆;
-
使用第三方工具实现深克隆,比如 Apache Commons Lang;
-
使用 JSON 工具类实现深克隆,比如 Gson、FastJSON 等。
1、所有对象都实现克隆
这种方式我们需要修改 People 和 Address 类,让它们都实现 Cloneable 的接口,让所有的引用对象都实现克隆,从而实现 People 类的深克隆,代码如下:
public class CloneExample {
public static void main(String[] args) throws CloneNotSupportedException {
// 创建被赋值对象
Address address = new Address(110, "北京");
People p1 = new People(1, "Java", address);
// 克隆 p1 对象
People p2 = p1.clone();
// 修改原型对象
p1.getAddress().setCity("西安");
// 输出 p1 和 p2 地址信息
System.out.println("p1:" + p1.getAddress().getCity() +
" p2:" + p2.getAddress().getCity());
}
/**
* 用户类
*/
static class People implements Cloneable {
private Integer id;
private String name;
private Address address;
/**
* 重写 clone 方法
* @throws CloneNotSupportedException
*/
@Override
protected People clone() throws CloneNotSupportedException {
People people = (People) super.clone();
people.setAddress(this.address.clone()); // 引用类型克隆赋值
return people;
}
// 忽略构造方法、set、get 方法
}
/**
* 地址类
*/
static class Address implements Cloneable {
private Integer id;
private String city;
/**
* 重写 clone 方法
* @throws CloneNotSupportedException
*/
@Override
protected Address clone() throws CloneNotSupportedException {
return (Address) super.clone();
}
// 忽略构造方法、set、get 方法
}
}
以上程序的执行结果为:
p1:西安 p2:北京
从结果可以看出,当我们修改了原型对象的引用属性之后,并没有影响克隆对象,这说明此对象已经实现了深克隆。
2、通过构造方法实现深克隆
《Effective Java》 中推荐使用构造器(Copy Constructor)来实现深克隆,如果构造器的参数为基本数据类型或字符串类型则直接赋值,如果是对象类型,则需要重新 new 一个对象,实现代码如下:
public class SecondExample {
public static void main(String[] args) throws CloneNotSupportedException {
// 创建对象
Address address = new Address(110, "北京");
People p1 = new People(1, "Java", address);
// 调用构造函数克隆对象
People p2 = new People(p1.getId(), p1.getName(),
new Address(p1.getAddress().getId(), p1.getAddress().getCity()));
// 修改原型对象
p1.getAddress().setCity("西安");
// 输出 p1 和 p2 地址信息
System.out.println("p1:" + p1.getAddress().getCity() +
" p2:" + p2.getAddress().getCity());
}
/**
* 用户类
*/
static class People {
private Integer id;
private String name;
private Address address;
// 忽略构造方法、set、get 方法
}
/**
* 地址类
*/
static class Address {
private Integer id;
private String city;
// 忽略构造方法、set、get 方法
}
}
以上程序的执行结果为:
p1:西安 p2:北京
从结果可以看出,当我们修改了原型对象的引用属性之后,并没有影响克隆对象,这说明此对象已经实现了深克隆。
3、通过字节流实现深克隆
通过 JDK 自带的字节流实现深克隆的方式,是先将要原型对象写入到内存中的字节流,然后再从这个字节流中读出刚刚存储的信息,来作为一个新的对象返回,那么这个新对象和原型对象就不存在任何地址上的共享,这样就实现了深克隆,代码如下:
import java.io.*;
public class ThirdExample {
public static void main(String[] args) throws CloneNotSupportedException {
// 创建对象
Address address = new Address(110, "北京");
People p1 = new People(1, "Java", address);
// 通过字节流实现克隆
People p2 = (People) StreamClone.clone(p1);
// 修改原型对象
p1.getAddress().setCity("西安");
// 输出 p1 和 p2 地址信息
System.out.println("p1:" + p1.getAddress().getCity() +
" p2:" + p2.getAddress().getCity());
}
/**
* 通过字节流实现克隆
*/
static class StreamClone {
public static <T extends Serializable> T clone(People obj) {
T cloneObj = null;
try {
// 写入字节流
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bo);
oos.writeObject(obj);
oos.close();
// 分配内存,写入原始对象,生成新对象
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());//获取上面的输出字节流
ObjectInputStream oi = new ObjectInputStream(bi);
// 返回生成的新对象
cloneObj = (T) oi.readObject();
oi.close();
} catch (Exception e) {
e.printStackTrace();
}
return cloneObj;
}
}
/**
* 用户类
*/
static class People implements Serializable {
private Integer id;
private String name;
private Address address;
// 忽略构造方法、set、get 方法
}
/**
* 地址类
*/
static class Address implements Serializable {
private Integer id;
private String city;
// 忽略构造方法、set、get 方法
}
}
以上程序的执行结果为:
p1:西安 p2:北京
此方式需要注意的是,由于是通过字节流序列化实现的深克隆,因此每个对象必须能被序列化,必须实现 Serializable 接口,标识自己可以被序列化,否则会抛出异常 (java.io.NotSerializableException)。
4、通过第三方工具实现深克隆
使用 Apache Commons Lang 来实现深克隆,实现代码如下:
import org.apache.commons.lang3.SerializationUtils;
import java.io.Serializable;
/**
* 深克隆实现方式四:通过 apache.commons.lang 实现
*/
public class FourthExample {
public static void main(String[] args) throws CloneNotSupportedException {
// 创建对象
Address address = new Address(110, "北京");
People p1 = new People(1, "Java", address);
// 调用 apache.commons.lang 克隆对象
People p2 = (People) SerializationUtils.clone(p1);
// 修改原型对象
p1.getAddress().setCity("西安");
// 输出 p1 和 p2 地址信息
System.out.println("p1:" + p1.getAddress().getCity() +
" p2:" + p2.getAddress().getCity());
}
/**
* 用户类
*/
static class People implements Serializable {
private Integer id;
private String name;
private Address address;
// 忽略构造方法、set、get 方法
}
/**
* 地址类
*/
static class Address implements Serializable {
private Integer id;
private String city;
// 忽略构造方法、set、get 方法
}
}
以上程序的执行结果为:
p1:西安 p2:北京
可以看出此方法和第三种实现方式类似,都需要实现 Serializable 接口,都是通过字节流的方式实现的,只不过这种实现方式是第三方提供了现成的方法,让我们可以直接调用。
5、通过 JSON 工具类实现深克隆
使用 Google 提供的 JSON 转化工具 Gson 来实现,其他 JSON 转化工具类也是类似的,实现代码如下:
import com.google.gson.Gson;
/**
* 深克隆实现方式五:通过 JSON 工具实现
*/
public class FifthExample {
public static void main(String[] args) throws CloneNotSupportedException {
// 创建对象
Address address = new Address(110, "北京");
People p1 = new People(1, "Java", address);
// 调用 Gson 克隆对象
Gson gson = new Gson();
People p2 = gson.fromJson(gson.toJson(p1), People.class);
// 修改原型对象
p1.getAddress().setCity("西安");
// 输出 p1 和 p2 地址信息
System.out.println("p1:" + p1.getAddress().getCity() +
" p2:" + p2.getAddress().getCity());
}
/**
* 用户类
*/
static class People {
private Integer id;
private String name;
private Address address;
// 忽略构造方法、set、get 方法
}
/**
* 地址类
*/
static class Address {
private Integer id;
private String city;
// 忽略构造方法、set、get 方法
}
}
以上程序的执行结果为:
p1:西安 p2:北京
使用 JSON 工具类会先把对象转化成字符串,再从字符串转化成新的对象,因为新对象是从字符串转化而来的,因此不会和原型对象有任何的关联,这样就实现了深克隆,其他类似的 JSON 工具类实现方式也是一样的。
整型包装类值的比较可以用 == 吗?
所有整型包装类对象值的比较必须使用 equals 方法。
这是因为当使用自动装箱方式创建一个 Integer 对象时,当数值在 -128 ~127 时,会将创建的 Integer 对象缓存起来,当下次再出现该数值时,直接从缓存中取出对应的 Integer 对象,因此可以直接用 == 比较。但是不在这个范围内的数值就不能用 == 比较了,需要使用 equals 方法比较数值。
在 jdk 1.8 所有的数值类缓冲池中,Integer 的缓冲池 IntegerCache 很特殊,这个缓冲池的下界是 - 128,上界默认是 127,但是这个上界是可调的,在启动 jvm 的时候,通过 -XX:AutoBoxCacheMax=size 来指定这个缓冲池的大小,该选项在 JVM 初始化的时候会设定一个名为 java.lang.IntegerCache.high 系统属性,然后 IntegerCache 初始化的时候就会读取该系统属性来决定上界。
new Integer() 与 Integer.valueOf() 的区别?
- new Integer() 每次都会新建一个对象;
- Integer.valueOf() 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
编译器会在自动装箱过程调用 valueOf() 方法,因此多个值相同且值在缓存池范围内的 Integer 实例使用自动装箱来创建,那么就会引用相同的对象。
浮点数如何做等值判断?
使用使用 BigDecimal 来定义浮点数的值,再进行浮点数的运算操作。
推荐使用如下三种方式来创建BigDecimal 对象:
- 直接传入字符串
- 传入数值转换成字符串
- 通过 BigDecima l的 valueOf 方法传入数值
示例:
public static void main(String[] args) {
float a = 1.0f - 0.9f;
float b = 0.9f - 0.8f;
System.out.println(a); // 0.100000024
System.out.println(b); // 0.099999964
System.out.println(a == b); // false
// 1.直接传入字符串
BigDecimal aa = new BigDecimal("1.0");
// 2.传入数值转换成字符串
BigDecimal bb = new BigDecimal(Double.toString(0.9));
// 3.通过BigDecimal的valueOf方法传入数值
BigDecimal cc = BigDecimal.valueOf(0.8);
BigDecimal x = aa.subtract(bb); // 0.1
BigDecimal y = bb.subtract(cc); // 0.1
System.out.println(x.equals(y)); // true
}
BigDecimal 主要用来操作(大)浮点数,BigInteger 主要用来操作大整数(超过 long 类型)。
BigDecimal 的实现利用到了 BigInteger,所不同的是 BigDecimal 加入了小数位的概念。
什么是动态代理,它和静态代理的区别?
动态代理可以在运行时动态创建一个类,实现一个或多个接口,可以在不修改原有类的基础上动态为通过该类获取的对象添加方法、修改行为。
静态代理需要为每一个代理对象创建一个代理类。
JDK 和 CGLib 动态代理的区别
JDK 动态代理
利用反射生成一个继承 Proxy 类且实现代理接口的匿名类。核心是实现InvocationHandler 接口,使用 invoke 方法进行面向切面的处理,调用相应通知。
步骤
- 创建一个实现
InvocationHandler的类,重写invoke方法 - 创建被代理类以及接口
- 通过
Proxy类的静态方法newProxyInstance创建一个代理类 - 通过代理类调用方法
// 接口
public interface FoodService {
public void makeNoodle();
public void makeChicken();
}
// 被代理类实现接口
public class FoodServiceImpl implements FoodService {
@Override
public void makeNoodle() {
System.out.println("make noodle");
}
@Override
public void makeChicken() {
System.out.println("make Chicken");
}
}
public class JDKProxyFactory implements InvocationHandler {
private Object target;
public JDKProxyFactory(Object target) {
super();
this.target = target;
}
// 创建代理对象
public Object createProxy() {
// 1.得到目标对象的类加载器
ClassLoader classLoader = target.getClass().getClassLoader();
// 2.得到目标对象的实现接口
Class<?>[] interfaces = target.getClass().getInterfaces();
// 3.第三个参数需要一个实现invocationHandler接口的对象
Object newProxyInstance = Proxy.newProxyInstance(classLoader, interfaces, this);
return newProxyInstance;
}
// 第一个参数:代理对象.一般不使用;第二个参数:需要增强的方法;第三个参数:方法中的参数
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("这是增强方法前......");
Object invoke = method.invoke(target, args);
System.out.println("这是增强方法后......");
return invoke;
}
public static void main(String[] args) {
// 1.创建对象
FoodServiceImpl foodService = new FoodServiceImpl();
// 2.创建代理对象
JDKProxyFactory proxy = new JDKProxyFactory(foodService);
// 3.调用代理对象的增强方法,得到增强后的对象
FoodService createProxy = (FoodService) proxy.createProxy();
createProxy.makeChicken();
}
}
CGLib 动态代理
CGLib 是第三方提供的工具,基于 ASM 实现的,把代理对象类的 class 文件加载进内存,通过修改其字节码生成代理类的子类来处理。核心是实现 MethodInterceptor 接口,使用 intercept 方法进行面向切面的处理,调用相应通知。它比使用 Java 反射的 JDK 动态代理要快。
public class CglibProxyFactory implements MethodInterceptor {
// 得到目标对象
private Object target;
// 使用构造方法传递目标对象
public CglibProxyFactory(Object target) {
super();
this.target = target;
}
// 创建代理对象
public Object createProxy(){
// 1.创建Enhancer
Enhancer enhancer = new Enhancer();
// 2.传递目标对象的class
enhancer.setSuperclass(target.getClass());
// 3.设置回调操作
enhancer.setCallback(this);
return enhancer.create();
}
// 参数一:代理对象;参数二:需要增强的方法;参数三:需要增强方法的参数;参数四:需要增强的方法的代理
public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
System.out.println("这是增强方法前......");
Object invoke = methodProxy.invoke(target, args);
System.out.println("这是增强方法后......");
return invoke;
}
public static void main(String[] args) {
// 1.创建对象
FoodServiceImpl foodService = new FoodServiceImpl();
// 2.创建代理对象
CglibProxyFactory proxy = new CglibProxyFactory(foodService);
// 3.调用代理对象的增强方法,得到增强后的对象
FoodService createProxy = (FoodService) proxy.createProxy();
createProxy.makeChicken();
}
}
区别
JDK 代理面向的是一组接口,它为这些接口动态创建了一个实现类。接口的具体实现逻辑是通过自定义的 InvocationHandler实现的,这个实现是自定义的,也就是说,其背后都不一定有真正被代理的对象,也可能有多个实际对象,根据情况动态选择。
CGLIB 代理面向的是一个具体的类,它动态创建了一个新类,继承了该类,重写了其方法。但是如果被代理类中有被 final 关键字所修饰的方法,该方法就无法重写。
在JDK6、JDK7、JDK8逐步对 JDK 动态代理优化之后,在调用次数较少的情况下,JDK 代理效率高于 CGLIB 代理效率,只有当进行大量调用的时候,JDK6 和 JDK7 比 CGLIB 代理效率低一点,但是到 JDK8 的时候 JDK 代理效率高于 CGLIB 代理。
在 Spring 中:
- 如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理实现 AOP。
- 如果目标对象实现了接口,可以强制使用 CGLIB 实现 AOP。
- 如果目标对象没有实现接口,必须采用 CGLIB 库,Spring 会自动在 JDK 动态代理和 CGLIB 之间转换。
什么是反射?什么是字节码增强?
反射指的是程序可以访问、检测和修改它本身状态或行为的一种能力。
反射主要有以下功能:
- 在运行时判断任意一个对象所属的类
- 在运行时构造任意一个类的对象
- 在运行时判断任意一个类所具有的成员变量和方法
- 在运行时调用任意一个对象的方法,通过反射甚至可以调用到私有方法
- 生成动态代理
字节码增强指的是在 Java 字节码生成之后,对其进行修改,增强其功能,这种方式相当于对应用程序的二进制文件进行修改。它比反射开销小,性能高。
两者区别
- 反射是读取持久堆上存储的类信息,而字节码增强可以直接处理
.class字节码。 - 反射只能读取类信息,而字节码增强除了读还能写。
- 反射读取类信息时需要进行类加载处理,而字节码增强不需要将类加载到内存中。
- 反射相对于字节码增强来说使用方便,想使用字节码增强的话需要有 JVM 指令基础。
什么是哈希算法
将任意长度的输入转换成固定长度的输出。