Appearance
Java 基础 - 知识点
本文主要对Java基础知识点进行总结。
数据类型
包装类型
Java 的八个基本数据类型分为四类:整数类型、浮点类型、字符类型和布尔类型。以下是这八个基本数据类型:
整数类型(Integral Types)
- byte
大小:8 位
范围:-128 到 127
- short
大小:16 位
范围:-32768 到 32767
- int
大小:32 位
范围:-2147483648 到 2147483647
- long
大小:64 位
范围:-9223372036854775808 到 9223372036854775807
浮点类型(Floating-Point Types)
float
大小:32 位
范围:约 ±3.40282347E+38F(有效位数为约 7 位)
double
大小:64 位
范围:约 ±1.79769313486231570E+308(有效位数为约 15 位)
字符类型(Character Type)
- char
大小:16 位
范围:'\u0000' 到 '\uffff',表示 Unicode 字符
布尔类型(Boolean Type)
- boolean
只有两个可能的值:true 或 false
基本类型都有对应的包装类型,基本类型与其对应的包装类型之间的赋值使用自动装箱与拆箱完成。
缓存池
缓存池是一种用于存储和重复使用对象的机制,以提高性能和减少资源消耗。缓存池通常用于存储那些创建成本较高的对象,以便在需要时可以直接获取而不是重新创建。
new Integer(123) 与 Integer.valueOf(123) 的区别在于:
- new Integer(123) 每次都会新建一个对象
- Integer.valueOf(123) 会使用缓存池中的对象,多次调用会取得同一个对象的引用。
java
Integer x = new Integer(123);
Integer y = new Integer(123);
System.out.println(x == y); // false
Integer z = Integer.valueOf(123);
Integer k = Integer.valueOf(123);
System.out.println(z == k); // true
valueOf() 方法的实现比较简单,就是先判断值是否在缓存池中,如果在的话就直接返回缓存池的内容。
java
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
在 Java 8 中,Integer 缓存池的大小默认为 -128~127。
java
static final int low = -128;
static final int high;
static final Integer cache[];
static {
// high value may be configured by property
int h = 127;
String integerCacheHighPropValue =
sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
if (integerCacheHighPropValue != null) {
try {
int i = parseInt(integerCacheHighPropValue);
i = Math.max(i, 127);
// Maximum array size is Integer.MAX_VALUE
h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
} catch( NumberFormatException nfe) {
// If the property cannot be parsed into an int, ignore it.
}
}
high = h;
cache = new Integer[(high - low) + 1];
int j = low;
for(int k = 0; k < cache.length; k++)
cache[k] = new Integer(j++);
// range [-128, 127] must be interned (JLS7 5.1.7)
assert IntegerCache.high >= 127;
}
编译器会在缓冲池范围内的基本类型自动装箱过程调用 valueOf() 方法,因此多个 Integer 实例使用自动装箱来创建并且值相同,那么就会引用相同的对象。
java
Integer m = 123;
Integer n = 123;
System.out.println(m == n); // true
基本类型对应的缓冲池如下:
- boolean values true and false
- all byte values
- short values between -128 and 127
- int values between -128 and 127
- char in the range \u0000 to \u007F
在使用这些基本类型对应的包装类型时,就可以直接使用缓冲池中的对象。
如果在缓冲池之外:
java
Integer m = 323;
Integer n = 323;
System.out.println(m == n); // false
String
String
是一个表示字符串的类,String 被声明为 final,因此它不可被继承。
内部使用 char 数组存储数据,该数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。
java
public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
/** The value is used for character storage. */
private final char value[];
不可变的好处
1. Hash 算法的安全性
String
类被广泛用于作为 HashMap
的键。由于字符串的不可变性,它们的 hashCode
值在创建时就确定了,不会在之后改变。这确保了在哈希集合中存储和检索字符串时的一致性。
2. 字符串池(String Pool)
不可变性使得字符串可以被缓存和共享,因为在运行时无法修改它们的值。如果一个 String 对象已经被创建过了,那么就会从 String Pool 中取得引用。只有 String 是不可变的,才可能使用 String Pool。
3. 安全性
String 经常作为参数,String 不可变性可以保证参数不可变。例如在作为网络连接参数的情况下如果 String 是可变的,那么在网络连接过程中,String 被改变,改变 String 对象的那一方以为现在连接的是其它主机,而实际情况却不一定是。
4. 线程安全
由于字符串是不可变的,多个线程可以同时访问和操作字符串对象而不会导致数据不一致或冲突。这使得字符串处理在多线程环境中更加安全,无需额外的同步措施。
String, StringBuffer and StringBuilder
String
、StringBuffer
和StringBuilder
都用于处理字符串,但它们有一些关键的区别。
String(字符串):
- 不可变性:
String
是不可变的,一旦创建,其值不能被修改。任何对字符串的修改都会创建一个新的字符串对象。 - 线程安全性:由于不可变性,
String
对象是线程安全的。多个线程可以同时访问和操作字符串对象而不会产生冲突。 - 字符串池:Java 中的字符串池允许相同的字符串值在内存中共享,以减少内存开销。
javaString str = "Hello";
- 不可变性:
StringBuffer:
- 可变性:与
String
不同,StringBuffer
是可变的。它允许在字符串的基础上进行添加、插入、删除等修改操作。 - 线程安全性:
StringBuffer
是线程安全的,支持多线程环境下的并发操作,因为它的方法都被synchronized
修饰。
javaStringBuffer buffer = new StringBuffer("Hello"); buffer.append(" World");
- 可变性:与
StringBuilder:
- 可变性:
StringBuilder
也是可变的,与StringBuffer
相似。但是,相对于StringBuffer
,StringBuilder
的方法没有被同步,因此在单线程环境中更高效。 - 非线程安全性:
StringBuilder
不是线程安全的,不适用于多线程环境,但在单线程场景下性能更好。
javaStringBuilder builder = new StringBuilder("Hello"); builder.append(" World");
- 可变性:
选择使用哪种类型取决于具体的需求:
- 如果你处理的字符串是固定的,不需要修改,且在多线程环境中使用,可以使用
String
。 - 如果需要频繁对字符串进行修改,并且在多线程环境下使用,可以选择
StringBuffer
。 - 如果在单线程环境下需要频繁修改字符串,可以选择
StringBuilder
,它的性能比StringBuffer
更好。
String
适用于不变的字符串,而StringBuffer
和StringBuilder
适用于需要可变字符串的场景,其中StringBuilder
更适合在单线程环境中使用。
String.intern()
String.intern()
是一个用于在字符串池中注册字符串的方法。它返回字符串池中与调用 intern()
方法的字符串内容相同的字符串引用。如果字符串池中已经存在相同内容的字符串,则返回池中的引用;否则,将该字符串添加到字符串池中,然后返回对该字符串的引用。
使用 String.intern()
方法可以有效地减少相同字符串的重复存储,从而节省内存。在某些场景中,特别是处理大量字符串且可能包含许多相同内容的情况下,使用 intern()
方法可以提高性能和降低内存消耗。
下面示例中,s1 和 s2 采用 new String() 的方式新建了两个不同对象,而 s3 是通过 s1.intern() 方法取得一个对象引用。intern() 首先把 s1 引用的对象放到 String Pool(字符串常量池)中,然后返回这个对象引用。因此 s3 和 s1 引用的是同一个字符串常量池的对象。
javaString s1 = new String("aaa"); String s2 = new String("aaa"); System.out.println(s1 == s2); // false String s3 = s1.intern(); System.out.println(s1.intern() == s3); // true
如果是采用 "bbb" 这种使用双引号的形式创建字符串实例,会自动地将新建的对象放入 String Pool 中。
javaString s4 = "bbb"; String s5 = "bbb"; System.out.println(s4 == s5); // true
- HotSpot中字符串常量池保存哪里?永久代?方法区还是堆区?
- 运行时常量池(Runtime Constant Pool)是虚拟机规范中是方法区的一部分,在加载类和结构到虚拟机后,就会创建对应的运行时常量池;而字符串常量池是这个过程中常量字符串的存放位置。所以从这个角度,字符串常量池属于虚拟机规范中的方法区,它是一个逻辑上的概念;而堆区,永久代以及元空间是实际的存放位置。
- 不同的虚拟机对虚拟机的规范(比如方法区)是不一样的,只有 HotSpot 才有永久代的概念。
- HotSpot也是发展的,由于一些问题的存在,HotSpot考虑逐渐去永久代,对于不同版本的JDK,实际的存储位置是有差异的,具体看如下表格:
JDK版本 | 是否有永久代,字符串常量池放在哪里? | 方法区逻辑上规范,由哪些实际的部分实现的? |
---|---|---|
jdk1.6及之前 | 有永久代,运行时常量池(包括字符串常量池),静态变量存放在永久代上 | 这个时期方法区在HotSpot中是由永久代来实现的,以至于这个时期说方法区就是指永久代 |
jdk1.7 | 有永久代,但已经逐步“去永久代”,字符串常量池、静态变量移除,保存在堆中; | 这个时期方法区在HotSpot中由永久代(类型信息、字段、方法、常量)和堆(字符串常量池、静态变量)共同实现 |
jdk1.8及之后 | 取消永久代,类型信息、字段、方法、常量保存在本地内存的元空间,但字符串常量池、静态变量仍在堆中 | 这个时期方法区在HotSpot中由本地内存的元空间(类型信息、字段、方法、常量)和堆(字符串常量池、静态变量)共同实现 |
运算
参数传递
Java 的参数是以值传递的形式传入方法中,而不是引用传递。
以下代码中 Dog dog 的 dog 是一个指针,存储的是对象的地址。在将一个参数传入一个方法时,本质上是将对象的地址以值的方式传递到形参中。因此在方法中改变指针引用的对象,那么这两个指针此时指向的是完全不同的对象,一方改变其所指向对象的内容对另一方没有影响。
javapublic class Dog { String name; Dog(String name) { this.name = name; } String getName() { return this.name; } void setName(String name) { this.name = name; } String getObjectAddress() { return super.toString(); } } public class PassByValueExample { public static void main(String[] args) { Dog dog = new Dog("A"); System.out.println(dog.getObjectAddress()); // Dog@4554617c func(dog); System.out.println(dog.getObjectAddress()); // Dog@4554617c System.out.println(dog.getName()); // A } private static void func(Dog dog) { System.out.println(dog.getObjectAddress()); // Dog@4554617c dog = new Dog("B"); System.out.println(dog.getObjectAddress()); // Dog@74a14482 System.out.println(dog.getName()); // B } }
但是如果在方法中改变对象的字段值会改变原对象该字段值,因为改变的是同一个地址指向的内容。
javaclass PassByValueExample { public static void main(String[] args) { Dog dog = new Dog("A"); func(dog); System.out.println(dog.getName()); // B } private static void func(Dog dog) { dog.setName("B"); } }
float 与 double
float
和 double
都是Java中用于表示浮点数的数据类型。它们主要的区别在于精度和存储空间。
float(单精度浮点数)
- 32 位(4 字节)。
- 精度约为 7 位有效数字。
java
float floatValue = 3.14f;
double(双精度浮点数)
- 64 位(8 字节)。
- 精度约为 15 位有效数字。
java
double doubleValue = 3.14;
主要区别和注意事项:
精度:
double
提供了更高的精度,因此通常在实际应用中更常用。对于大多数计算,double
是更好的选择,特别是在科学计算或需要高精度表示的场景。存储空间:
float
使用较少的存储空间,但牺牲了精度。double
使用更多的存储空间,但提供更高的精度。在内存空间充足的情况下,可以优先选择double
。默认类型:Java 中浮点数的字面量默认是
double
类型。如果要使用float
类型的字面量,需要在数字后面添加f
或F
。javafloat floatValue = 3.14f;
类型转换:如果将
double
赋给float
,需要进行强制类型转换。因为double
的范围和精度可能超过float
。javadouble doubleValue = 3.14; float floatValue = (float) doubleValue;
选择使用 float
还是 double
取决于具体的需求。在一般情况下,如果需要更高的精度,可以选择 double
。如果内存空间有限或者精度要求不是很高,可以选择 float
。
隐式类型转换
隐式类型转换(Implicit Type Conversion) 也被称为自动类型转换或拓宽转换,是指在不需要进行显式操作的情况下,由编译器自动完成的类型转换。这通常发生在将数据范围小的类型赋值给数据范围大的类型时。
以下是一些常见的隐式类型转换的情况:
小范围类型到大范围类型:将小范围的整数类型(如
byte
、short
、char
)赋值给大范围的整数类型(如int
、long
)。javabyte b = 10; int i = b; // 隐式类型转换
整数类型到浮点数类型:将整数类型(如
int
、long
)赋值给浮点数类型(如float
、double
)。javaint num = 42; double d = num; // 隐式类型转换
字符类型到整数类型:将
char
类型的字符赋值给整数类型。javachar c = 'A'; int asciiValue = c; // 隐式类型转换
小范围浮点数类型到大范围浮点数类型:将小范围的浮点数类型(如
float
)赋值给大范围的浮点数类型(如double
)。javafloat f = 3.14f; double d = f; // 隐式类型转换
隐式类型转换只能在从小范围到大范围的方向进行,不会丢失精度。如果从大范围到小范围进行赋值,就需要进行显式的类型转换,可能会导致数据丢失或溢出。
java
double bigValue = 123.45;
int intValue = (int) bigValue; // 显式类型转换,可能导致精度损失
在进行类型转换时,要注意避免数据溢出和精度丢失的问题,确保转换是安全的。
switch
switch
是一种用于多分支条件选择的语句。它提供了一种更简洁的方式来处理多个可能的条件。
基本的 switch
语法如下:
java
switch (expression) {
case value1:
// 代码块1
break;
case value2:
// 代码块2
break;
// 可以有多个 case
default:
// 默认代码块
}
expression
是一个表达式,它的结果会与每个case
的值进行比较。case
后面是可能的值,如果expression
的值与某个case
的值匹配,就会执行相应的代码块。break
语句用于跳出switch
语句,如果没有break
,将会继续执行后面的case
或default
。default
是可选的,用于处理没有匹配到任何case
的情况。
以下是一个简单的
switch
语句的示例:javapublic class SwitchExample { public static void main(String[] args) { int day = 3; switch (day) { case 1: System.out.println("Monday"); break; case 2: System.out.println("Tuesday"); break; case 3: System.out.println("Wednesday"); break; case 4: System.out.println("Thursday"); break; case 5: System.out.println("Friday"); break; default: System.out.println("Weekend"); } } }
在这个例子中,根据
day
的值,会打印出对应的星期几。如果day
的值没有匹配到任何case
,则会执行default
中的代码块。
需要注意
switch
语句只支持整数类型(包括枚举类型)、字符类型和字符串类型。从Java 7 开始,switch
语句也支持字符串类型。在Java 12 中,引入了对 switch 表达式的增强,允许使用更灵活的语法。
继承
访问权限
Java 中有四种访问权限修饰符,用于控制类、变量、方法和构造方法的访问级别。这些权限修饰符分别是:
public(公共): 公共访问级别,对所有类都是可见的。被声明为 public 的类、方法、变量可以被任何其他类访问。
protected(受保护): 受保护访问级别,对同一包内的类和所有子类可见。如果不在同一包内,子类只能访问其继承的 protected 成员,而不能访问类中其他 protected 成员。
default(默认): 默认访问级别,不使用任何修饰符。对同一包内的类可见,但对子类不可见(即使子类在不同的包中)。
private(私有): 私有访问级别,只对同一类内可见。被声明为 private 的方法、变量、构造方法只能被所属类访问,其他类无法访问。
这些访问权限修饰符可用于类的成员(字段、方法、构造方法)上,以控制其他类对这些成员的访问权限。例如:
java
public class MyClass {
public int publicVar;
protected int protectedVar;
int defaultVar; // 默认访问级别
private int privateVar;
public void publicMethod() {
// 公共方法的实现
}
protected void protectedMethod() {
// 受保护方法的实现
}
void defaultMethod() {
// 默认访问级别方法的实现
}
private void privateMethod() {
// 私有方法的实现
}
}
请注意,访问权限修饰符的选择应该根据设计需求和封装的原则进行,以确保良好的软件设计和安全性。
抽象类与接口
在Java中,抽象类和接口是两种实现抽象类型的机制。它们都用于实现面向对象编程的抽象和封装的概念,但它们有一些区别。
抽象类(Abstract Class)
抽象类和抽象方法都使用 abstract 关键字进行声明。抽象类一般会包含抽象方法,抽象方法一定位于抽象类中。
抽象类和普通类最大的区别是,抽象类不能被实例化,需要继承抽象类才能实例化其子类。
关键特点:
- 可以包含抽象方法和具体方法。
- 可以有构造方法。
- 可以有实例变量(字段)。
- 可以有普通方法(非抽象方法)。
- 一个类只能继承一个抽象类。
使用场景:
- 当多个类有共同的实现,但其中一部分实现需要在子类中进行特定实现时,可以使用抽象类。
- 当需要在基类中提供一些通用的方法实现时。
例子:
javaabstract class Shape { int x, y; Shape(int x, int y) { this.x = x; this.y = y; } abstract void draw(); // 抽象方法 } class Circle extends Shape { int radius; Circle(int x, int y, int radius) { super(x, y); this.radius = radius; } @Override void draw() { // 具体实现 System.out.println("Drawing Circle"); } }
接口(Interface)
关键特点:
- 只能包含抽象方法,不包含字段。
- 不允许定义构造方法。
- 所有的字段都是
public
、static
、final
的(隐式)。 - 一个类可以实现多个接口。
使用场景:
- 当多个类需要实现相同的契约,但它们可能属于不同的继承体系时,可以使用接口。
- 当希望实现多重继承时,因为Java不支持多重继承,接口可以用来弥补这一缺陷。
例子:
javainterface Drawable { void draw(); // 抽象方法 } class Circle implements Drawable { int x, y, radius; Circle(int x, int y, int radius) { this.x = x; this.y = y; this.radius = radius; } @Override public void draw() { // 具体实现 System.out.println("Drawing Circle"); } }
比较
- 从设计层面上看,抽象类提供了一种 IS-A 关系,那么就必须满足里式替换原则,即子类对象必须能够替换掉所有父类对象。而接口更像是一种 LIKE-A 关系,它只是提供一种方法实现契约,并不要求接口和实现接口的类具有 IS-A 关系。
- 从使用上来看,一个类可以实现多个接口,但是不能继承多个抽象类。
- 接口的字段只能是 static 和 final 类型的,而抽象类的字段没有这种限制。
- 接口的成员只能是 public 的,而抽象类的成员可以有多种访问权限。
使用选择
使用接口:
- 需要让不相关的类都实现一个方法,例如不相关的类都可以实现
Compareable
接口中的compareTo()
方法; - 需要使用多重继承。
使用抽象类:
- 需要在几个相关的类中共享代码。
- 需要能控制继承来的成员的访问权限,而不是都为 public。
- 需要继承非静态和非常量字段。
在很多情况下,接口优先于抽象类,因为接口没有抽象类严格的类层次结构要求,可以灵活地为一个类添加行为。并且从 Java 8 开始,接口也可以有默认的方法实现,使得修改接口的成本也变的很低。
super
super
关键字用于访问父类的成员(字段或方法),它有两种主要的用途:
访问父类的字段:
使用
super
可以在子类中访问父类的字段。示例:
javaclass Animal { String name = "Animal"; } class Dog extends Animal { String name = "Dog"; void printNames() { System.out.println("Subclass name: " + name); // 访问子类的字段 System.out.println("Superclass name: " + super.name); // 访问父类的字段 } }
在上面的例子中,
Dog
类继承自Animal
类,通过super.name
可以访问父类Animal
的name
字段。
调用父类的方法:
使用
super
可以在子类中调用父类的方法。示例:
javaclass Animal { void makeSound() { System.out.println("Animal makes a sound"); } } class Dog extends Animal { @Override void makeSound() { super.makeSound(); // 调用父类的方法 System.out.println("Dog barks"); } }
在上面的例子中,
Dog
类重写了父类Animal
的makeSound
方法,并使用super.makeSound()
调用了父类的实现。
重写和重载
重写(Override)和重载(Overload)是Java中两个不同的概念,它们涉及到方法的定义和使用。
重写(Override)
子类中定义一个与父类中具有相同名称和参数列表的方法,称为方法的重写。
- 特点:
- 发生在继承关系中,子类覆盖父类的方法。
- 重写的方法的方法名、参数列表、返回类型必须与父类中被重写的方法一致。
- 重写的方法不能拥有更严格的访问权限,但可以拥有更宽松的访问权限(例如,父类方法是
protected
,子类可以重写为public
)。 - 重写的方法不能抛出比父类方法更多的异常。
例子:
java
class Animal {
void makeSound() {
System.out.println("Animal makes a sound");
}
}
class Dog extends Animal {
@Override
void makeSound() {
System.out.println("Dog barks");
}
}
重载(Overload)
在同一个类中,可以定义多个具有相同名称但参数列表不同(个数、类型或顺序)的方法,称为方法的重载。
- 特点:
- 发生在同一个类中。
- 重载的方法的方法名相同,但参数列表不同。
- 重载方法的返回类型可以相同也可以不同。
- 重载方法的访问权限、异常类型可以相同也可以不同。
例子:
java
class Calculator {
int add(int a, int b) {
return a + b;
}
double add(double a, double b) {
return a + b;
}
}
总结
- 重写关注的是父类和子类之间的继承关系,子类覆盖父类的方法。
- 重载关注的是同一个类中,同一个方法名的多个版本,它们的参数列表不同。
Object 通用方法
java
public final native Class<?> getClass()
public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native void notify()
public final native void notifyAll()
public final native void wait(long timeout) throws InterruptedException
public final void wait(long timeout, int nanos) throws InterruptedException
public final void wait() throws InterruptedException
protected void finalize() throws Throwable {}
equals()
equals()
方法,用于比较对象的内容是否相等。默认情况下,equals()
方法在Object
类中的实现是比较两个对象的引用是否相等,即比较内存地址。
java
public boolean equals(Object obj) {
return (this == obj);
}
然而,通常我们在自定义类中需要重写 equals()
方法以实现对象内容的比较而不仅仅是引用的比较。在重写 equals()
方法时,需要遵循以下几个约定:
- 自反性(Reflexive): 对于任何非空引用值
x
,x.equals(x)
应该返回true
。 - 对称性(Symmetric): 对于任何非空引用值
x
和y
,如果x.equals(y)
返回true
,那么y.equals(x)
也应该返回true
。 - 传递性(Transitive): 对于任何非空引用值
x
、y
和z
,如果x.equals(y)
返回true
,并且y.equals(z)
也返回true
,那么x.equals(z)
应该返回true
。 - 一致性(Consistent): 对于任何非空引用值
x
和y
,多次调用x.equals(y)
应该始终返回相同的结果,前提是对象没有被修改。 - 对
null
的比较: 对于任何非空引用值x
,x.equals(null)
应该返回false
。
2. equals() 与 ==
- 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
- 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价。
java
Integer x = new Integer(1);
Integer y = new Integer(1);
System.out.println(x.equals(y)); // true
System.out.println(x == y); // false
3. 实现
- 检查是否为同一个对象的引用,如果是直接返回 true;
- 检查是否是同一个类型,如果不是,直接返回 false;
- 将 Object 对象进行转型;
- 判断每个关键域是否相等。
下面是一个示例演示如何在自定义类中重写
equals()
方法:javaclass Person { String name; int age; public Person(String name, int age) { this.name = name; this.age = age; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; Person person = (Person) obj; return age == person.age && name.equals(person.name); } }
在上述例子中,
equals()
方法比较了两个Person
对象的name
和age
字段。
hashCode()
用于返回对象的哈希码值(32位整数)。哈希码是一种用于提高数据检索效率的技术,它将对象映射为一个整数,使得相等的对象具有相等的哈希码。
默认情况下,Object
类中的hashCode()
方法返回的哈希码是基于对象的内存地址的,即每个对象都有唯一的哈希码。但在实际使用中,通常需要在自定义类中重写hashCode()
方法,以确保相等的对象具有相等的哈希码。
hashCode() 返回散列值,而 equals() 是用来判断两个对象是否等价。等价的两个对象散列值一定相同,但是散列值相同的两个对象不一定等价。
在覆盖 equals() 方法时应当总是覆盖 hashCode() 方法,保证等价的两个对象散列值也相等。
下面的代码中,新建了两个等价的对象,并将它们添加到 HashSet 中。我们希望将这两个对象当成一样的,只在集合中添加一个对象,但是因为 EqualExample 没有实现 hasCode() 方法,因此这两个对象的散列值是不同的,最终导致集合添加了两个等价的对象。
java
EqualExample e1 = new EqualExample(1, 1, 1);
EqualExample e2 = new EqualExample(1, 1, 1);
System.out.println(e1.equals(e2)); // true
HashSet<EqualExample> set = new HashSet<>();
set.add(e1);
set.add(e2);
System.out.println(set.size()); // 2
理想的散列函数应当具有均匀性,即不相等的对象应当均匀分布到所有可能的散列值上。这就要求了散列函数要把所有域的值都考虑进来,可以将每个域都当成 R 进制的某一位,然后组成一个 R 进制的整数。R 一般取 31,因为它是一个奇素数,如果是偶数的话,当出现乘法溢出,信息就会丢失,因为与 2 相乘相当于向左移一位。
一个数与 31 相乘可以转换成移位和减法: 31*x == (x<<5)-x
,编译器会自动进行这个优化。
java
@Override
public int hashCode() {
int result = 17;
result = 31 * result + x;
result = 31 * result + y;
result = 31 * result + z;
return result;
}
toString()
默认返回 ToStringExample@4554617c 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
java
public class ToStringExample {
private int number;
public ToStringExample(int number) {
this.number = number;
}
}
ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());
ToStringExample@4554617c
clone()
1. cloneable
clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法。
java
public class CloneExample {
private int a;
private int b;
}
CloneExample e1 = new CloneExample();
// CloneExample e2 = e1.clone(); // 'clone()' has protected access in 'java.lang.Object'
重写 clone() 得到以下实现:
java
public class CloneExample {
private int a;
private int b;
@Override
protected CloneExample clone() throws CloneNotSupportedException {
return (CloneExample)super.clone();
}
}
CloneExample e1 = new CloneExample();
try {
CloneExample e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
java.lang.CloneNotSupportedException: CloneExample
以上抛出了 CloneNotSupportedException,这是因为 CloneExample 没有实现 Cloneable 接口。
应该注意的是,clone() 方法并不是 Cloneable 接口的方法,而是 Object 的一个 protected 方法。Cloneable 接口只是规定,如果一个类没有实现 Cloneable 接口又调用了 clone() 方法,就会抛出 CloneNotSupportedException。
java
public class CloneExample implements Cloneable {
private int a;
private int b;
@Override
protected Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
2. 浅拷贝
拷贝对象和原始对象的引用类型引用同一个对象。
java
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();
}
}
java
ShallowCloneExample e1 = new ShallowCloneExample();
ShallowCloneExample e2 = null;
try {
e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 222
3. 深拷贝
拷贝对象和原始对象的引用类型引用不同对象。
java
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;
}
}
java
DeepCloneExample e1 = new DeepCloneExample();
DeepCloneExample e2 = null;
try {
e2 = e1.clone();
} catch (CloneNotSupportedException e) {
e.printStackTrace();
}
e1.set(2, 222);
System.out.println(e2.get(2)); // 2
4. clone() 的替代方案
使用 clone() 方法来拷贝一个对象即复杂又有风险,它会抛出异常,并且还需要类型转换。Effective Java 书上讲到,最好不要去使用 clone(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象。
java
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];
}
}
java
CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1);
e1.set(2, 222);
System.out.println(e2.get(2)); // 2