深拷贝和浅拷贝
-
浅拷贝:浅拷贝会在堆上创建一个新的对象(区别于引用拷贝的一点),不过,如果原对象内部的属性是引用类型的话,浅拷贝会直接复制内部对象的引用地址,也就是说拷贝对象和原对象共用同一个内部对象。
-
深拷贝 :深拷贝会完全复制整个对象,包括这个对象所包含的内部对象。
-
引用拷贝:两个不同的引用指向同一个对象
public class Person implements Cloneable { private Address address; // 省略构造函数、Getter&Setter方法 @Override public Person clone() { try { Person person = (Person) super.clone(); return person; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } } //测试 Person person1 = new Person(new Address("武汉")); Person person1Copy = person1.clone(); // true 从输出结构就可以看出, person1 的克隆对象和 person1 使用的仍然是同一个 Address 对象。 System.out.println(person1.getAddress() == person1Copy.getAddre ss()); //深拷贝 这里我们简单对 Person 类的 clone() 方法进行修改,连带着要把 Person 对象内部的 Address 对象一起复制。 @Override public Person clone() { try { Person person = (Person) super.clone(); person.setAddress(person.getAddress().clone()); return person; } catch (CloneNotSupportedException e) { throw new AssertionError(); } } //测试 Person person1 = new Person(new Address("武汉")); Person person1Copy = person1.clone(); // false 从输出结构就可以看出,虽然 person1 的克隆对象和 person1 包含的 Address 对象已经是不同的了。 System.out.println(person1.getAddress() == person1Copy.getAddress());
String
中的equals
方法是被重写过的,因为Object
的equals
方法是比较的对象的内存地址,而String
的equals
方法比较的是对象的值。
hashCode()
hashCode()
的作用是获取哈希码(int
整数),也称为散列码。这个哈希码的作用是确定该对象在哈希表中的索引位置。散列表存储的是键值对(key-value),它的特点是:能根据“键”快速的检索出对应的“值”。这其中就利用到了散列码!(可以快速找到所需要的对象)
当你把对象加入
HashSet
时,HashSet
会先计算对象的hashCode
值来判断对象加入的位置,同时也会与其他已经加入的对象的hashCode
值作比较,如果没有相符的hashCode
,HashSet
会假设对象没有重复出现。但是如果发现有相同hashCode
值的对象,这时会调用equals()
方法来检查hashCode
相等的对象是否真的相同。如果两者相同,HashSet
就不会让其加入操作成功。如果不同的话,就会重新散列到其他位置。这样我们就大大减少了equals
的次数,相应就大大提高了执行速度。
两个对象的
hashCode
值相等并不代表两个对象就相等。
因为hashCode()
所使用的哈希算法也许刚好会让多个对象传回相同的哈希值。越糟糕的哈希算法越容易碰撞,但这也与数据值域分布的特性有关(所谓哈希碰撞也就是指的是不同的对象得到相同的hashCode
)。
总结下来就是 :
- 如果两个对象的
hashCode
值相等,那这两个对象不一定相等(哈希碰撞)。- 如果两个对象的
hashCode
值相等并且equals()
方法也返回true
,我们才认为这两个对象相等。- 如果两个对象的
hashCode
值不相等,我们就可以直接认为这两个对象不相等。
String、StringBuffer、StringBuilder
String
中的对象是不可变的,也就可以理解为常量,线程安全,StringBuffer
对方法加了同步锁或者对调用的方法加了同步锁,所以是线程安全的。StringBuilder
并没有对方法进行加同步锁,所以是非线程安全的。每次对 String
类型进行改变的时候,都会生成一个新的 String
对象,然后将指针指向新的 String
对象。StringBuffer
每次都会对 StringBuffer
对象本身进行操作,而不是生成新的对象并改变对象引用。相同情况下使用 StringBuilder
相比使用 StringBuffer
仅能获得 10%~15% 左右的性能提升,但却要冒多线程不安全的风险。
String 为什么是不可变的
被
final
关键字修饰的类不能被继承,修饰的方法不能被重写,修饰的变量是基本数据类型则值不能改变,修饰的变量是引用类型则不能再指向其他对象。因此,final
关键字修饰的数组保存字符串并不是String
不可变的根本原因,因为这个数组保存的字符串是可变的(final
修饰引用类型变量的情况)。
String
真正不可变有下面几点原因:
- 保存字符串的数组被
final
修饰且为私有的,并且String
类没有提供/暴露修改这个字符串的方法。String
类被final
修饰导致其不能被继承,进而避免了子类破坏String
不可变。
在进行字符串拼接的时候使用“+”和StringBuilder中的append效果是一致的。字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder
调用 append()
方法实现的,拼接完成之后调用 toString()
得到一个 String
对象 。不过,在循环内使用“+”进行字符串的拼接的话,存在比较明显的缺陷:编译器不会创建单个 StringBuilder
以复用,会导致创建过多的 StringBuilder
对象。
如何使用 try-with-resources
代替try-catch-finally
- 适用范围(资源的定义): 任何实现
java.lang.AutoCloseable
或者java.io.Closeable
的对象 - 关闭资源和 finally 块的执行顺序: 在
try-with-resources
语句中,任何 catch 或 finally 块在声明的资源关闭后运行
Java 中类似于InputStream
、OutputStream
、Scanner
、PrintWriter
等的资源都需要我们调用close()
方法来手动关闭,一般情况下我们都是通过try-catch-finally
语句来实现这个需求,如下:
//读取文本文件的内容
Scanner scanner = null;
try {
scanner = new Scanner(new File("D://read.txt"));
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException e) {
e.printStackTrace();
} finally {
if (scanner != null) {
scanner.close();
}
}
使用 Java 7 之后的 try-with-resources
语句改造上面的代码:
try (Scanner scanner = new Scanner(new File("test.txt"))) {
while (scanner.hasNext()) {
System.out.println(scanner.nextLine());
}
} catch (FileNotFoundException fnfe) {
fnfe.printStackTrace();
}
当然多个资源需要关闭的时候,使用 try-with-resources
实现起来也非常简单,如果你还是用try-catch-finally
可能会带来很多问题。
//通过使用分号分隔,可以在try-with-resources块中声明多个资源。
try (BufferedInputStream bin = new BufferedInputStream(new FileInputStream(new File("test.txt")));
BufferedOutputStream bout = new BufferedOutputStream(new FileOutputStream(new File("out.txt")))) {
int b;
while ((b = bin.read()) != -1) {
bout.write(b);
}
}
catch (IOException e) {
e.printStackTrace();
}
反射
反射之所以被称为框架的灵魂,主要是因为它赋予了我们在运行时分析类以及执行类中方法的能力。通过反射你可以获取任意一个类的所有属性和方法,你还可以调用这些方法和属性。
- 优点 : 可以让咱们的代码更加灵活、为各种框架提供开箱即用的功能提供了便利
- 缺点 :让我们在运行时有了分析操作类的能力,这同样也增加了安全问题。比如可以无视泛型参数的安全检查(泛型参数的安全检查发生在编译时)。另外,反射的性能也要稍差点,不过,对于框架来说实际是影响不大的。
框架中也大量使用了动态代理,而动态代理的实现也依赖反射
I/O
-
序列化: 将数据结构或对象转换成二进制字节流的过程
-
反序列化:将在序列化过程中所生成的二进制字节流转换成数据结构或者对象的过程
序列化的主要目的是通过网络传输对象或者说是将对象存储到文件系统、数据库、内存中。
对于不想进行序列化的变量,使用 transient
关键字修饰。transient
关键字的作用是:阻止实例中那些用此关键字修饰的的变量序列化;当对象被反序列化时,被 transient
修饰的变量值不会被持久化和恢复。
transient
只能修饰变量,不能修饰类和方法。transient
修饰的变量,在反序列化后变量值将会被置成类型的默认值。例如,如果是修饰int
类型,那么反序列后结果就是0
。static
变量因为不属于任何对象(Object),所以无论有没有transient
关键字修饰,均不会被序列化。
- InputStream/Reader: 所有的输入流的基类,前者是字节输入流,后者是字符输入流。
- OutputStream/Writer: 所有输出流的基类,前者是字节输出流,后者是字符输出流。
不管是文件读写还是网络发送接收,信息的最小存储单元都是字节,那为什么 I/O 流操作要分为字节流操作和字符流操作呢?
字符流是由 Java 虚拟机将字节转换得到的,问题就出在这个过程还算是非常耗时,并且,如果我们不知道编码类型就很容易出现乱码问题。所以, I/O 流就干脆提供了一个直接操作字符的接口,方便我们平时对字符进行流操作。如果音频文件、图片等媒体文件用字节流比较好,如果涉及到字符的话使用字符流比较好。
- 节点流可以从一个特定的数据源读写数据,如:FileReader、FileWriter
- 处理流(叶叫包装流)是连接在以存在的流(节点流或处理流)之上,为程序提供更为强大的读写功能,更加灵活多变。如BufferReader、BufferWriter。
重点
- 值传递 :方法接收的是实参值的拷贝,会创建副本。
- 引用传递 :方法接收的直接是实参所引用的对象在堆中的地址,不会创建副本,对形参的修改将影响到实参。
为什么说 Java 只有值传递呢?
通过上面例子,我们已经知道了一个方法不能修改一个基本数据类型的参数,而对象引用作为参数就不一样,请看案例2。
public static void main(String[] args) { int[] arr = { 1, 2, 3, 4, 5 }; System.out.println(arr[0]); change(arr); System.out.println(arr[0]); } public static void change(int[] array) { // 将数组的第一个元素变为0 array[0] = 0; } //输出:1,0
看了这个案例很多人肯定觉得 Java 对引用类型的参数采用的是引用传递。实际上,并不是的,这里传递的还是值,不过,这个值是实参的地址罢了!
public class Person { private String name; // 省略构造函数、Getter&Setter方法 } public static void main(String[] args) { Person xiaoZhang = new Person("小张"); Person xiaoLi = new Person("小李"); swap(xiaoZhang, xiaoLi); System.out.println("xiaoZhang:" + xiaoZhang.getName()); System.out.println("xiaoLi:" + xiaoLi.getName()); } public static void swap(Person person1, Person person2) { Person temp = person1; person1 = person2; person2 = temp; System.out.println("person1:" + person1.getName()); System.out.println("person2:" + person2.getName()); }
输出:
person1:小李 person2:小张 xiaoZhang:小张 xiaoLi:小李 //两个引用类型的形参互换并没有影响实参啊
swap
方法的参数person1
和person2
只是拷贝的实参xiaoZhang
和xiaoLi
的地址。因此,person1
和person2
的互换只是拷贝的两个地址的互换罢了,并不会影响到实参xiaoZhang
和xiaoLi
。
Java 中将实参传递给方法(或函数)的方式是 值传递 :
- 如果参数是基本类型的话,很简单,传递的就是基本类型的字面量值的拷贝,会创建副本。
- 如果参数是引用类型,传递的就是实参所引用的对象在堆中地址值的拷贝,同样也会创建副本。
序列化
- 对象在进行网络传输(比如远程方法调用 RPC 的时候)之前需要先被序列化,接收到序列化的对象之后需要再进行反序列化;
- 将对象存储到文件中的时候需要进行序列化,将对象从文件中读取出来需要进行反序列化。
- 将对象存储到缓存数据库(如 Redis)时需要用到序列化,将对象从缓存数据库中读取出来需要反序列化