四、关键字
Java关键字是Java编程语言中保留的词汇,这些词汇具有特殊的意义和用途,用于定义数据类型、控制程序的流程、声明类和方法等。Java关键字不能作为变量名、方法名、类名或任何其他标识符使用。下面是一些常见的Java关键字及其简要说明:
-
数据类型关键字:
boolean
:布尔类型,取值为true
或false
。byte
:8位整数类型。char
:16位Unicode字符。short
:16位整数类型。int
:32位整数类型。long
:64位整数类型。float
:32位单精度浮点数。double
:64位双精度浮点数。
-
访问控制关键字:
public
:公开的,可以被所有类访问。protected
:受保护的,可以被同一包中的类以及子类访问。private
:私有的,只能被声明它的类访问。
-
类、方法和变量修饰符:
abstract
:声明抽象类或抽象方法。class
:定义一个类。extends
:表示一个类是另一个类的子类。final
:声明最终类、方法或变量,不能被继承、重写或修改。implements
:声明一个类实现一个或多个接口。interface
:定义一个接口。native
:声明本地方法,用于与本地代码(如C/C++)进行交互。static
:声明类的静态成员,属于类而不是实例。strictfp
:用于限制浮点计算,使其在所有平台上有一致的结果。synchronized
:用于方法或代码块,表示同步,防止线程干扰。transient
:声明不序列化的变量。volatile
:声明易变的变量,防止编译器进行优化。
-
控制流程关键字:
break
:跳出当前循环或switch语句。case
:定义switch语句中的一个分支。continue
:跳过当前循环的剩余部分并开始下一次循环。default
:定义switch语句中的默认分支。do
:定义do-while循环的开始。else
:与if语句配合使用,定义条件为false时的执行代码。for
:定义for循环。if
:定义条件语句。instanceof
:测试对象是否是特定类的实例。return
:从方法返回值。switch
:定义多分支选择结构。while
:定义while循环。
-
异常处理关键字:
try
:定义一个异常处理块的开始。catch
:定义捕获异常的代码块。finally
:定义在try-catch结构中总是执行的代码块。throw
:抛出一个异常。throws
:声明一个方法可能抛出的异常。
-
其他关键字:
import
:导入其他包中的类或接口。package
:声明类所在的包。super
:引用父类的成员。this
:引用当前对象的成员。void
:声明方法没有返回值。
这些关键字是Java语言的基础,理解它们的用法和作用是学习Java编程的重要一步。
final
在Java编程中,final
关键字具有多种用途,其核心作用是赋予不可变性和确保约束。具体而言,final
可以用于修饰变量、方法和类。
final
变量 :- 修饰基本类型变量时,一旦被初始化后,变量的值便不能再更改。
- 修饰引用类型变量时,一旦被初始化后,引用不能被更改,但所引用对象的状态可以改变。典型的用法是在类中定义常量,通常结合
static
关键字使用,如public static final int MAX_SIZE = 100;
。
final
方法 :- 使方法不能被子类重写(override)。这在设计API时尤为重要,能够防止子类改变父类中关键方法的行为,从而保证类的设计初衷和稳定性。
final
类 :- 表示类不能被继承。这通常用于设计不可变类(immutable class)或者为了安全考虑,防止扩展。比如,Java标准库中的
java.lang.String
类就是一个final
类。
- 表示类不能被继承。这通常用于设计不可变类(immutable class)或者为了安全考虑,防止扩展。比如,Java标准库中的
使用final
的好处包括:
- 安全性:防止类被继承、方法被重写、变量值被修改,有助于保护数据的完整性。
- 效率 :编译器和JVM可以进行更多优化,例如将
final
变量内联,提高执行效率。
然而,过度使用final
也可能带来灵活性不足的问题,因此应在设计时权衡使用。
final
可用于声明不可变的实体,可以用于变量、方法和类的声明上:
-
修饰类:当用
final
修饰一个类时,表示该类不可被继承,即该类时最终的,不能有子类!javafinal class FinalClass { // 类定义 }
-
修饰方法:当
final
修饰一个方法时,表示该方法不能被子类重写(覆盖)。javaclass ParentClass { final void finalMethod() { // 方法实现 } }
-
修饰变量:当用
final
修饰一个变量时,表示该变量的值只能被赋值一次,之后不能再修改。对于基本数据类型,表示其值不能改变;对于引用类型,表示其引用不可变,但对象的内容可以改变。javafinal int x = 10; // 基本数据类型 final MyClass obj = new MyClass(); // 引用类型
final
关键字的使用有助于编写更加安全、可靠和清晰的代码,它可以在一定程序上提高代码的可读性和可维护性。
1. 数据
声明数据为常量,可以是编译时常量 ,也可以是在运行时被初始化后不能被改变的常量。
- 对于基本类型,final 使数值不变;
- 对于引用类型,final 使引用不变 ,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
java
final int x = 1;
// x = 2; // cannot assign value to final variable 'x'
final A y = new A();
y.a = 1;
2. 方法
声明方法不能被子类重写。
private 方法隐式地被指定为 final:
- 如果在子类中定义的方法和基类中的一个 private 方法签名相同
- 此时子类的方法不是重写基类方法
- 而是在子类中定义了一个新的方法。
3. 类
声明类不允许被继承。
final的实现
那么final怎么实现的?
final
关键字的实现依赖于Java编译器和Java虚拟机(JVM)。在Java源代码编译成字节码以及字节码被JVM执行的过程中,final
关键字的语义通过编译器和JVM的支持来实现。下面是详细的实现机制:
1.基本类型变量
对于基本类型变量,final
关键字在编译期确保变量在初始化后不能被重新赋值。这是通过编译器的检查来实现的。
实现细节:
- 编译器检查 :在编译阶段,编译器会检查
final
变量是否在声明时或通过构造函数进行了一次且仅一次的赋值。如果发现尝试重新赋值的代码,编译器将报错。 - 字节码生成 :编译器生成的字节码会标记该变量为
final
,以防止在运行时进行修改。
java
public class FinalExample {
public static void main(String[] args) {
final int x = 10;
// cannot assign a value to final variable
x = 20; // 编译错误
}
}
2.引用类型变量
对于引用类型变量,final
关键字确保引用一旦初始化后不能指向另一个对象,但引用的对象内容可以改变。
实现细节:
- 编辑器检查:同样,编译器会确保引用类型变量在声明时或通过构造函数进行了一次且仅一次的赋值。
- 字节码生成 :引用变量被标记为
final
,防止在运行时改变其指向。
java
public class FinalExample {
public static void main(String[] args) {
final List<String> list = new ArrayList<>();
list.add("Hello"); // 允许
list = new ArrayList<>(); // 编译错误
}
}
3.final
方法
final
方法不能被子类重写。这个限制通过编译器和JVM来实现。
实现细节
- 编译器检查 :在编译阶段,编译器会确保在子类中不能重新标记为
final
的方法。 - 字节码生成 :在字节码中,
final
方法会被标记。JVM在运行时加载类时会检查这个标记,确保方法不会被重写。
示例代码
java
public class Parent {
public final void show() {
System.out.println("Parent show()");
}
}
public class Child extends Parent {
// cannot overridden method is final
public void show() { // 编译错误
System.out.println("Child show()");
}
}
4.final
类
final
类不能被继承。这个限制同样通过编译器和JVM来实现。
实现细节
- 编译器检查:在编译阶段,编译器会确保没有类可以继承标记为
final
的类。 - 字节码生成:在字节码中。
final
类会标记。JVM在运行时加载类时会检查这个标记,确保类不会被继承。
示例代码
java
public final class FinalClass {
// 类体
}
// cannot inherit from final class
public class SubClass extends FinalClass { // 编译错误
// 类体
}
总结
编译器和JVM的配合
编译器负责在编译阶段执行静态检查,并在生成的字节码中包含适当的标记。JVM在运行时通过字节码中的这些标记来执行相应的约束。
- 编译期约束 :在编译期,
final
关键字的约束确保程序的静态正确性。任何违反final
规则的代码都不会通过编译。 - 运行期约束 :在运行期,JVM利用字节码中的
final
标记来确保变量、方法或类的不可变性。这种机制保证了final
语义在整个程序生命周期内的一致性。
内联优化
由于final
变量和方法的不可变性,JVM和Just-In-Time(JIT)编译器可以对它们进行优化,如内联优化。这种优化能够提高程序的执行效率。
实现细节:
- 编译器优化 :编译器在编译期可以将
final
变量直接替换为其值,从而减少内存访问。 - JIT编译器优化 :JIT编译器在运行时可以将
final
方法内联到调用点,减少方法调用的开销。
总结
final
关键字的实现依赖于Java编译器和JVM的合作,通过在编译期和运行期的检查和优化,确保其语义得到正确执行。它不仅提高了程序的安全性和稳定性,还为性能优化提供了可能。
final
= 断子绝孙?
为什么final常被称为标识断子绝孙?
"断子绝孙"这个说法形象的描述了使用final
关键字的效果,意思是通过使用final
可以在当前类或实体上施加限制,阻止其被子类或后续继承者修改或扩展。
这种说法在某些情况下是合适的。例如,在设计框架和库时,如果某个类、方法或变量的行为已经非常稳定,并且不希望被修改,可以使用final
来确保其不被继承或修改。这样可以有效地确保代码的稳定性和安全性。
注意
过度使用final
有可能会导致代码的灵活性降低 ,因为它限制了后续的修改和扩展 。因此,在使用final
时需要权衡利弊,根据实际情况来决定是否使用以及使用的范围和方式。
static
static
是Java中的一个关键字,用于创建类变量和类方法,它具有以下几个重要的特性:
- 类变量(静态变量):使用
static
关键字声明的类级别的变量。 - 类方法(静态方法):使用
static
关键字声明的方法是类级别的方法。 - 静态代码块:使用
static
关键字声明的静态代码块在类加载时执行,并且只执行一次。
1. 静态变量
- 静态变量 :又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量 ,可以直接通过类名来访问它。静态变量在内存中只存在一份。
- 实例变量 :每创建一个实例就会产生一个实例变量,它与该实例同生共死。
java
public class A {
private int x; // 实例变量
private static int y; // 静态变量
public static void main(String[] args) {
// int x = A.x; // Non-static field 'x' cannot be referenced from a static context
A a = new A();
int x = a.x;
int y = A.y;
}
}
2.静态方法
静态方法在类加载的时候就存在了 ,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。
java
public abstract class A {
public static void func1(){
}
// public abstract static void func2(); // Illegal combination of modifiers: 'abstract' and 'static'
}
只能访问所属类的静态字段和静态方法 ,方法中不能有 this
和 super
关键字,因为这两个关键字与具体对象关联。
java
public class A {
private static int x;
private int y;
public static void func1(){
int a = x;
// int b = y; // Non-static field 'y' cannot be referenced from a static context
// int b = this.y; // 'A.this' cannot be referenced from a static context
}
}
3. 静态语句块
静态语句块在类初始化时运行一次。
java
public class A {
static {
System.out.println("123");
}
public static void main(String[] args) {
A a1 = new A();
A a2 = new A();
}
}
java
123
4. 静态内部类
非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。
而静态内部类不需要
java
public class OuterClass {
class InnerClass {
}
static class StaticInnerClass {
}
public static void main(String[] args) {
// InnerClass innerClass = new InnerClass(); // 'OuterClass.this' cannot be referenced from a static context
OuterClass outerClass = new OuterClass();
InnerClass innerClass = outerClass.new InnerClass();
StaticInnerClass staticInnerClass = new StaticInnerClass();
}
}
静态内部类:不能访问外部类的非静态的变量和方法。
5. 静态导包
在使用静态变量和方法时不用再指明 ClassName
,从而简化代码,但可读性大大降低。
java
import static com.xxx.ClassName.*
6. 初始化顺序
(静态变量和静态语句块 )优先于(实例变量和普通语句块 ),静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序。
java
public static String staticField = "静态变量";
java
static {
System.out.println("静态语句块");
}
java
public String field = "实例变量";
java
{
System.out.println("普通语句块");
}
最后才是构造函数的初始化。
java
public InitialOrderTest() {
System.out.println("构造函数");
}
Java 类的初始化顺序
在 Java 中,当涉及继承时,类的初始化顺序遵循特定的规则。这里将详细整理这一过程,包括静态变量、静态代码块、实例变量、实例代码块和构造函数的初始化顺序。
初始化顺序
-
父类的静态变量和静态代码块:
- 静态变量和静态代码块在类加载时初始化,并且仅执行一次。
- 先按声明顺序执行父类的静态变量初始化和静态代码块。
-
子类的静态变量和静态代码块:
- 静态变量和静态代码块在类加载时初始化,并且仅执行一次。
- 先按声明顺序执行子类的静态变量初始化和静态代码块。
-
父类的实例变量和实例代码块:
- 每次创建实例时,都会执行实例变量初始化和实例代码块。
- 先按声明顺序执行父类的实例变量初始化和实例代码块。
-
父类的构造函数:
- 在父类实例变量和实例代码块执行完毕后,调用父类的构造函数。
-
子类的实例变量和实例代码块:
- 每次创建实例时,都会执行实例变量初始化和实例代码块。
- 先按声明顺序执行子类的实例变量初始化和实例代码块。
-
子类的构造函数:
- 在子类实例变量和实例代码块执行完毕后,调用子类的构造函数。
举例说明
java
public class StaticTestParent {
static int parentVariable = print("父类静态变量初始化");
static {
print("父类静态代码块初始化");
}
/**
* 实例变量和实例代码块
*/
int instanceVarParent = print("父类实例变量初始化");
{
print("父类实例代码块初始化");
}
/**
* 构造函数
*/
StaticTestParent() {
print("父类构造函数");
}
/**
* 打印方法
*
* @param message 消息
* @return 打印后返回值
*/
static int print(String message) {
System.out.println(message);
return 0;
}
}
class StaticTest extends StaticTestParent {
/**
* 静态变量和静态代码块
*/
static int staticVarChild = print("子类静态变量初始化");
static { print("子类静态代码块初始化"); }
/**
* 实例变量和实例代码块
*/
int instanceVarChild = print("子类实例变量初始化");
{ print("子类实例代码块初始化"); }
/**
* 构造函数
*/
StaticTest() {
print("子类构造函数");
}
}
/**
* @author hao
*/
class Main {
public static void main(String[] args) {
new StaticTest();
}
}
输出结果
tex
父类静态变量初始化
父类静态代码块初始化
子类静态变量初始化
子类静态代码块初始化
父类实例变量初始化
父类实例代码块初始化
父类构造函数
子类实例变量初始化
子类实例代码块初始化
子类构造函数
解释
- 父类的静态变量和静态代码块 首先执行:
父类静态变量初始化
和父类静态代码块初始化
。 - 子类的静态变量和静态代码块 接着执行:
子类静态变量初始化
和子类静态代码块初始化
。 - 父类的实例变量和实例代码块 在实例创建时执行:
父类实例变量初始化
和父类实例代码块初始化
。 - 父类的构造函数 在实例变量和代码块执行完后调用:
父类构造函数
。 - 子类的实例变量和实例代码块 接着执行:
子类实例变量初始化
和子类实例代码块初始化
。 - 子类的构造函数 最后调用:
子类构造函数
。
这种顺序确保了父类的静态成员在子类之前初始化,父类的实例成员在子类实例成员之前初始化,从而保证了正确的初始化顺序和对象的状态。
Object
Object 通用方法
1、概览
java
public native int hashCode()
public boolean equals(Object obj)
protected native Object clone() throws CloneNotSupportedException
public String toString()
public final native Class<?> getClass()
protected void finalize() throws Throwable {}
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
2、概述
在Java中,object
是一个非常重要的关键字,因为它是所有类的祖先类。任何类,无论是否显式继承其他类,最终都继承自java.lang.Object
类。这意味着Object
类中的方法可以被所有Java对象使用和覆盖。以下是Object
类的几个关键方法及其用途:
-
equals(Object obj)
:- 用于比较两个对象是否"逻辑相等"。
- 默认实现是比较对象的内存地址(引用),子类通常会覆盖这个方法来实现值比较。
java@Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null || getClass() != obj.getClass()) return false; MyClass myClass = (MyClass) obj; return Objects.equals(field, myClass.field); }
-
hashCode()
:- 返回对象的哈希码,通常与
equals
方法一起覆盖,以保证相等的对象具有相同的哈希码。 - 默认实现基于对象的内存地址。
java@Override public int hashCode() { return Objects.hash(field); }
- 返回对象的哈希码,通常与
-
toString()
:- 返回对象的字符串表示,默认实现返回对象的类名和内存地址。
-
通常会被覆盖以提供更有意义的字符串表示。
java@Override public String toString() { return "MyClass{" + "field='" + field + '\'' + '}'; }
-
getClass()
:- 返回对象的运行时类,是一个
final
方法,不能被覆盖。
javaClass<?> clazz = obj.getClass();
- 返回对象的运行时类,是一个
-
-
clone()
:- 创建并返回对象的一个副本。对象必须实现
Cloneable
接口并覆盖此方法。 - 默认实现是浅复制。
java@Override protected Object clone() throws CloneNotSupportedException { return super.clone(); }
- 创建并返回对象的一个副本。对象必须实现
-
finalize()
:- 对象被垃圾回收器回收之前调用的方法,通常不推荐使用,因为垃圾回收机制是不确定的且不可预测的。
java@Override protected void finalize() throws Throwable { // Cleanup code here super.finalize(); }
-
wait()
,notify()
,notifyAll()
:- 用于线程间通信,通常与
synchronized
关键字一起使用,来实现线程的等待与通知机制。
javasynchronized (obj) { obj.wait(); } synchronized (obj) { obj.notify(); } synchronized (obj) { obj.notifyAll(); }
- 用于线程间通信,通常与
这些方法是Object
类的重要组成部分,通过理解和正确使用它们,可以编写出更高效、更健壮的Java代码。
3、equals()
1. 等价关系
两个对象具有等价关系,需要满足以下五个条件:
Ⅰ 自反性
java
x.equals(x); // true
Ⅱ 对称性
java
x.equals(y) == y.equals(x); // true
Ⅲ 传递性
java
if (x.equals(y) && y.equals(z))
x.equals(z); // true;
Ⅳ 一致性
多次调用 equals() 方法结果不变
java
x.equals(y) == x.equals(y); // true
Ⅴ 与 null 的比较
对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false
java
x.equals(null); // false;
2. 等价与相等
- 对于基本类型,== 判断两个值是否相等,基本类型没有 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 对象进行转型;
- 判断每个关键域是否相等。
java
public class EqualExample {
private int x;
private int y;
private int z;
public EqualExample(int x, int y, int z) {
this.x = x;
this.y = y;
this.z = z;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
EqualExample that = (EqualExample) o;
if (x != that.x) return false;
if (y != that.y) return false;
return z == that.z;
}
}
4、hashCode()
hashCode()
返回哈希值,而 equals()
是用来判断两个对象是否等价。
等价的两个对象散列值一定相同 ,但是散列值相同的两个对象不一定等价 ,这是因为计算哈希值具有随机性,两个值不同的对象可能计算出相同的哈希值。
在覆盖
equals()
方法时应当总是覆盖hashCode()
方法,保证等价的两个对象哈希值也相等。
HashSet
和 HashMap
等集合类使用了 hashCode()
方法来计算对象应该存储的位置,因此要将对象添加到这些集合类中,需要让对应的类实现 hashCode()
方法。
下面的代码中:
- 新建了两个等价的对象,并将它们添加到
HashSet
中。 - 我们希望将这两个对象当成一样的,只在集合中添加一个对象。
- 但是
EqualExample
没有实现hashCode()
方法,因此这两个对象的哈希值是不同的,最终导致集合添加了两个等价的对象。
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;
}
equals,hashCode
关于
equals
和hashCode
方法之间关系的理解
equals
和hashCode
的契约
- 一致性原则 :如果两个对象根据
equals
方法被认为是相等的,那么它们必须具有相同的哈希码。这是hashCode
和equals
方法之间的基本契约。 - 哈希码的必要性 :哈希码是哈希表(如
HashSet
、HashMap
等)中用于快速查找的关键。一个对象的哈希码决定了它在哈希表中的存储桶位置。
- 默认行为
如果你只重写了equals
方法而没有重写hashCode
方法,默认的hashCode
方法将基于对象的内存地址生成哈希码。这样会导致两个逻辑相等的对象(即equals
返回true
)有不同的哈希码,从而破坏了哈希表的正确工作。
- 重写
hashCode
方法的重要性
当你重写equals
方法时,你需要确保hashCode
方法也能正确反映对象的相等性。这通常意味着你需要在hashCode
方法中使用与equals
方法相同的属性。通过这样做,你保证了:
- 如果两个对象是相等的(根据
equals
方法),它们的哈希码也是相同的。 - 如果两个对象是不相等的,它们的哈希码不一定不同,但不同的哈希码会减少哈希冲突,提高哈希表的性能。
示例
假设有一个类Person
,其equals
方法比较name
和age
属性:
java
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name, age);
}
}
在这个例子中,hashCode
方法使用了name
和age
属性,这与equals
方法中的比较逻辑是一致的。这样,如果两个Person
对象的name
和age
相同,它们的hashCode
返回值也会相同。
错误示例
如果你只使用了一个属性来生成哈希码,但在equals
方法中使用了多个属性进行比较,这会导致不一致。例如:
java
public class Person {
private String name;
private int age;
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
return Objects.hash(name); // 错误示例,只使用name属性
}
}
在这个例子中,两个name
相同但age
不同的对象会有相同的哈希码,但根据equals
方法它们是不相等的。这会导致哈希表在查找和存储这些对象时出现问题。
结论
正确地重写hashCode
方法时,需要确保它使用的属性与equals
方法中使用的属性一致,以维持对象的逻辑相等性和哈希码的一致性。这样可以确保在使用基于哈希表的数据结构时,能正确地插入、查找和删除对象。
5、toString()
默认返回 ToStringExample@4554617c
这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示。
java
public class ToStringExample {
private int number;
public ToStringExample(int number) {
this.number = number;
}
}
java
ToStringExample example = new ToStringExample(123);
System.out.println(example.toString());
java
ToStringExample@4554617c
6、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
public 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
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
那么clone()方法到底有什么作用呢?为什么我们要使用clone()方法呢
答:clone() 将一个对象的值复制到另一个对象。 clone() 方法节省了用于创建对象的精确副本的额外处理任务。
正如您在下面的示例中看到的,两个引用变量具有相同的值。
java
class Student18 implements Cloneable {
int rollno;
String name;
Student18(int rollno, String name) {
this.rollno = rollno;
this.name = name;
}
public static void main(String args[]) {
try {
Student18 s1 = new Student18(101, "amit");
Student18 s2 = (Student18) s1.clone();
System.out.println(s1.rollno + " " + s1.name);
System.out.println(s2.rollno + " " + s2.name);
} catch (CloneNotSupportedException c) {
}
}
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
输出 :
java
101 amit
101 amit
如果我们通过 new 关键字创建另一个对象并将另一个对象的值分配给这个对象,则需要对该对象进行大量处理。因此,为了节省额外的处理任务,我们使用 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();
}
}
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;
}
}
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];
}
}
CloneConstructorExample e1 = new CloneConstructorExample();
CloneConstructorExample e2 = new CloneConstructorExample(e1);
e1.set(2, 222);
System.out.println(e2.get(2)); // 2