Java基础——四、关键字

18 篇文章 0 订阅
11 篇文章 0 订阅

四、关键字

Java关键字是Java编程语言中保留的词汇,这些词汇具有特殊的意义和用途,用于定义数据类型、控制程序的流程、声明类和方法等。Java关键字不能作为变量名、方法名、类名或任何其他标识符使用。下面是一些常见的Java关键字及其简要说明:

  1. 数据类型关键字

    • boolean:布尔类型,取值为truefalse
    • byte:8位整数类型。
    • char:16位Unicode字符。
    • short:16位整数类型。
    • int:32位整数类型。
    • long:64位整数类型。
    • float:32位单精度浮点数。
    • double:64位双精度浮点数。
  2. 访问控制关键字

    • public:公开的,可以被所有类访问。
    • protected:受保护的,可以被同一包中的类以及子类访问。
    • private:私有的,只能被声明它的类访问。
  3. 类、方法和变量修饰符

    • abstract:声明抽象类或抽象方法。
    • class:定义一个类。
    • extends:表示一个类是另一个类的子类。
    • final:声明最终类、方法或变量,不能被继承、重写或修改。
    • implements:声明一个类实现一个或多个接口。
    • interface:定义一个接口。
    • native:声明本地方法,用于与本地代码(如C/C++)进行交互。
    • static:声明类的静态成员,属于类而不是实例。
    • strictfp:用于限制浮点计算,使其在所有平台上有一致的结果。
    • synchronized:用于方法或代码块,表示同步,防止线程干扰。
    • transient:声明不序列化的变量。
    • volatile:声明易变的变量,防止编译器进行优化。
  4. 控制流程关键字

    • break:跳出当前循环或switch语句。
    • case:定义switch语句中的一个分支。
    • continue:跳过当前循环的剩余部分并开始下一次循环。
    • default:定义switch语句中的默认分支。
    • do:定义do-while循环的开始。
    • else:与if语句配合使用,定义条件为false时的执行代码。
    • for:定义for循环。
    • if:定义条件语句。
    • instanceof:测试对象是否是特定类的实例。
    • return:从方法返回值。
    • switch:定义多分支选择结构。
    • while:定义while循环。
  5. 异常处理关键字

    • try:定义一个异常处理块的开始。
    • catch:定义捕获异常的代码块。
    • finally:定义在try-catch结构中总是执行的代码块。
    • throw:抛出一个异常。
    • throws:声明一个方法可能抛出的异常。
  6. 其他关键字

    • import:导入其他包中的类或接口。
    • package:声明类所在的包。
    • super:引用父类的成员。
    • this:引用当前对象的成员。
    • void:声明方法没有返回值。

这些关键字是Java语言的基础,理解它们的用法和作用是学习Java编程的重要一步。

final

在Java编程中,final关键字具有多种用途,其核心作用是赋予不可变性和确保约束。具体而言,final可以用于修饰变量、方法和类。

  1. final变量
    • 修饰基本类型变量时,一旦被初始化后,变量的值便不能再更改。
    • 修饰引用类型变量时,一旦被初始化后,引用不能被更改,但所引用对象的状态可以改变。典型的用法是在类中定义常量,通常结合static关键字使用,如public static final int MAX_SIZE = 100;
  2. final方法
    • 使方法不能被子类重写(override)。这在设计API时尤为重要,能够防止子类改变父类中关键方法的行为,从而保证类的设计初衷和稳定性。
  3. final
    • 表示类不能被继承。这通常用于设计不可变类(immutable class)或者为了安全考虑,防止扩展。比如,Java标准库中的java.lang.String类就是一个final类。

使用final的好处包括:

  • 安全性:防止类被继承、方法被重写、变量值被修改,有助于保护数据的完整性。
  • 效率:编译器和JVM可以进行更多优化,例如将final变量内联,提高执行效率。

然而,过度使用final也可能带来灵活性不足的问题,因此应在设计时权衡使用。

final可用于声明不可变的实体,可以用于变量、方法和类的声明上:

  1. 修饰类:当用final修饰一个类时,表示该类不可被继承,即该类时最终的,不能有子类!

    1. final class FinalClass {
          // 类定义
      }
      
  2. 修饰方法:当final修饰一个方法时,表示该方法不能被子类重写(覆盖)。

    1. class ParentClass {
          final void finalMethod() {
              // 方法实现
          }
      }
      
  3. 修饰变量:当用final修饰一个变量时,表示该变量的值只能被赋值一次,之后不能再修改。对于基本数据类型,表示其值不能改变;对于引用类型,表示其引用不可变,但对象的内容可以改变。

    1. final int x = 10; // 基本数据类型
      final MyClass obj = new MyClass(); // 引用类型
      

final关键字的使用有助于编写更加安全、可靠和清晰的代码,它可以在一定程序上提高代码的可读性和可维护性。

1. 数据

声明数据为常量,可以是编译时常量也可以是在运行时被初始化后不能被改变的常量

  • 对于基本类型,final 使数值不变
  • 对于引用类型,final 使引用不变,也就不能引用其它对象,但是被引用的对象本身是可以修改的。
final int x = 1;
// x = 2;  // cannot assign value to final variable 'x'
final A y = new A();
y.a = 1;

2. 方法

声明方法不能被子类重写

private 方法隐式地被指定为 final:

  1. 如果在子类中定义的方法和基类中的一个 private 方法签名相同
  2. 此时子类的方法不是重写基类方法
  3. 而是在子类中定义了一个新的方法。

3. 类

声明类不允许被继承。

final的实现

那么final怎么实现的?

final关键字的实现依赖于Java编译器和Java虚拟机(JVM)。在Java源代码编译成字节码以及字节码被JVM执行的过程中,final关键字的语义通过编译器和JVM的支持来实现。下面是详细的实现机制:

1.基本类型变量

对于基本类型变量,final关键字在编译期确保变量在初始化后不能被重新赋值。这是通过编译器的检查来实现的。

实现细节:

  • 编译器检查:在编译阶段,编译器会检查final变量是否在声明时或通过构造函数进行了一次且仅一次的赋值。如果发现尝试重新赋值的代码,编译器将报错。
  • 字节码生成:编译器生成的字节码会标记该变量为final,以防止在运行时进行修改。
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,防止在运行时改变其指向。
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在运行时加载类时会检查这个标记,确保方法不会被重写。

示例代码

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在运行时加载类时会检查这个标记,确保类不会被继承。

示例代码

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中的一个关键字,用于创建类变量和类方法,它具有以下几个重要的特性:

  1. 类变量(静态变量):使用static关键字声明的类级别的变量。
  2. 类方法(静态方法):使用static关键字声明的方法是类级别的方法。
  3. 静态代码块:使用static关键字声明的静态代码块在类加载时执行,并且只执行一次。
1. 静态变量
  • 静态变量:又称为类变量,也就是说这个变量属于类的,类所有的实例都共享静态变量,可以直接通过类名来访问它。静态变量在内存中只存在一份
  • 实例变量:每创建一个实例就会产生一个实例变量,它与该实例同生共死
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.静态方法

静态方法在类加载的时候就存在了,它不依赖于任何实例。所以静态方法必须有实现,也就是说它不能是抽象方法。

public abstract class A {
    public static void func1(){
    }
    // public abstract static void func2();  // Illegal combination of modifiers: 'abstract' and 'static'
}

只能访问所属类的静态字段和静态方法,方法中不能有 thissuper 关键字,因为这两个关键字与具体对象关联。

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. 静态语句块

静态语句块在类初始化时运行一次

public class A {
    static {
        System.out.println("123");
    }

    public static void main(String[] args) {
        A a1 = new A();
        A a2 = new A();
    }
}
123
4. 静态内部类

非静态内部类依赖于外部类的实例,也就是说需要先创建外部类实例,才能用这个实例去创建非静态内部类。

而静态内部类不需要

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,从而简化代码,但可读性大大降低。

import static com.xxx.ClassName.*
6. 初始化顺序

静态变量和静态语句块)优先于(实例变量和普通语句块),静态变量和静态语句块的初始化顺序取决于它们在代码中的顺序

public static String staticField = "静态变量";
static {
    System.out.println("静态语句块");
}
public String field = "实例变量";
{
    System.out.println("普通语句块");
}

最后才是构造函数的初始化

public InitialOrderTest() {
    System.out.println("构造函数");
}
Java 类的初始化顺序

在 Java 中,当涉及继承时,类的初始化顺序遵循特定的规则。这里将详细整理这一过程,包括静态变量、静态代码块、实例变量、实例代码块和构造函数的初始化顺序。

初始化顺序
  1. 父类的静态变量和静态代码块

    • 静态变量和静态代码块在类加载时初始化,并且仅执行一次。
    • 先按声明顺序执行父类的静态变量初始化和静态代码块。
  2. 子类的静态变量和静态代码块

    • 静态变量和静态代码块在类加载时初始化,并且仅执行一次。
    • 先按声明顺序执行子类的静态变量初始化和静态代码块。
  3. 父类的实例变量和实例代码块

    • 每次创建实例时,都会执行实例变量初始化和实例代码块。
    • 先按声明顺序执行父类的实例变量初始化和实例代码块。
  4. 父类的构造函数

    • 在父类实例变量和实例代码块执行完毕后,调用父类的构造函数。
  5. 子类的实例变量和实例代码块

    • 每次创建实例时,都会执行实例变量初始化和实例代码块。
    • 先按声明顺序执行子类的实例变量初始化和实例代码块。
  6. 子类的构造函数

    • 在子类实例变量和实例代码块执行完毕后,调用子类的构造函数。
举例说明
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();
    }
}

输出结果

父类静态变量初始化
父类静态代码块初始化
子类静态变量初始化
子类静态代码块初始化
父类实例变量初始化
父类实例代码块初始化
父类构造函数
子类实例变量初始化
子类实例代码块初始化
子类构造函数

解释

  1. 父类的静态变量和静态代码块首先执行:父类静态变量初始化父类静态代码块初始化
  2. 子类的静态变量和静态代码块接着执行:子类静态变量初始化子类静态代码块初始化
  3. 父类的实例变量和实例代码块在实例创建时执行:父类实例变量初始化父类实例代码块初始化
  4. 父类的构造函数在实例变量和代码块执行完后调用:父类构造函数
  5. 子类的实例变量和实例代码块接着执行:子类实例变量初始化子类实例代码块初始化
  6. 子类的构造函数最后调用:子类构造函数

这种顺序确保了父类的静态成员在子类之前初始化,父类的实例成员在子类实例成员之前初始化,从而保证了正确的初始化顺序和对象的状态。

Object

Object 通用方法

1、概览
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类的几个关键方法及其用途:

  1. equals(Object obj):

    • 用于比较两个对象是否“逻辑相等”。
    • 默认实现是比较对象的内存地址(引用),子类通常会覆盖这个方法来实现值比较。
    @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);
    }
    
  2. hashCode():

    • 返回对象的哈希码,通常与equals方法一起覆盖,以保证相等的对象具有相同的哈希码。
    • 默认实现基于对象的内存地址。
    @Override
    public int hashCode() {
        return Objects.hash(field);
    }
    
  3. toString():

    • 返回对象的字符串表示,默认实现返回对象的类名和内存地址。
  • 通常会被覆盖以提供更有意义的字符串表示。

    @Override
    public String toString() {
        return "MyClass{" +
               "field='" + field + '\'' +
               '}';
    }
    
   
4. **`getClass()`**:
   - 返回对象的运行时类,是一个`final`方法,不能被覆盖。

   ```java
   Class<?> clazz = obj.getClass();
  1. clone():

    • 创建并返回对象的一个副本。对象必须实现Cloneable接口并覆盖此方法。
    • 默认实现是浅复制。
    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
    
  2. finalize():

    • 对象被垃圾回收器回收之前调用的方法,通常不推荐使用,因为垃圾回收机制是不确定的且不可预测的。
    @Override
    protected void finalize() throws Throwable {
        // Cleanup code here
        super.finalize();
    }
    
  3. wait(), notify(), notifyAll():

    • 用于线程间通信,通常与synchronized关键字一起使用,来实现线程的等待与通知机制。
    synchronized (obj) {
        obj.wait();
    }
    
    synchronized (obj) {
        obj.notify();
    }
    
    synchronized (obj) {
        obj.notifyAll();
    }
    

这些方法是Object类的重要组成部分,通过理解和正确使用它们,可以编写出更高效、更健壮的Java代码。

3、equals()

1. 等价关系

两个对象具有等价关系,需要满足以下五个条件:

自反性

x.equals(x); // true

对称性

x.equals(y) == y.equals(x); // true

传递性

if (x.equals(y) && y.equals(z))
    x.equals(z); // true;

一致性

多次调用 equals() 方法结果不变

x.equals(y) == x.equals(y); // true

与 null 的比较

对任何不是 null 的对象 x 调用 x.equals(null) 结果都为 false

x.equals(null); // false;

2. 等价与相等

  • 对于基本类型,== 判断两个值是否相等,基本类型没有 equals() 方法。
  • 对于引用类型,== 判断两个变量是否引用同一个对象,而 equals() 判断引用的对象是否等价
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 对象进行转型;
  • 判断每个关键域是否相等。
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() 方法,保证等价的两个对象哈希值也相等

HashSetHashMap 等集合类使用了 hashCode() 方法来计算对象应该存储的位置,因此要将对象添加到这些集合类中,需要让对应的类实现 hashCode() 方法。

下面的代码中:

  1. 新建了两个等价的对象,并将它们添加到 HashSet 中。
  2. 我们希望将这两个对象当成一样的,只在集合中添加一个对象。
  3. 但是 EqualExample 没有实现 hashCode() 方法,因此这两个对象的哈希值是不同的,最终导致集合添加了两个等价的对象。
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,编译器会自动进行这个优化。

@Override
public int hashCode() {
    int result = 17;
    result = 31 * result + x;
    result = 31 * result + y;
    result = 31 * result + z;
    return result;
}
equals,hashCode

关于equalshashCode方法之间关系的理解

  1. equalshashCode的契约
  • 一致性原则:如果两个对象根据equals方法被认为是相等的,那么它们必须具有相同的哈希码。这是hashCodeequals方法之间的基本契约。
  • 哈希码的必要性:哈希码是哈希表(如HashSetHashMap等)中用于快速查找的关键。一个对象的哈希码决定了它在哈希表中的存储桶位置。
  1. 默认行为

如果你只重写了equals方法而没有重写hashCode方法,默认的hashCode方法将基于对象的内存地址生成哈希码。这样会导致两个逻辑相等的对象(即equals返回true)有不同的哈希码,从而破坏了哈希表的正确工作。

  1. 重写hashCode方法的重要性

当你重写equals方法时,你需要确保hashCode方法也能正确反映对象的相等性。这通常意味着你需要在hashCode方法中使用与equals方法相同的属性。通过这样做,你保证了:

  • 如果两个对象是相等的(根据equals方法),它们的哈希码也是相同的。
  • 如果两个对象是不相等的,它们的哈希码不一定不同,但不同的哈希码会减少哈希冲突,提高哈希表的性能。

示例

假设有一个类Person,其equals方法比较nameage属性:

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方法使用了nameage属性,这与equals方法中的比较逻辑是一致的。这样,如果两个Person对象的nameage相同,它们的hashCode返回值也会相同。

错误示例

如果你只使用了一个属性来生成哈希码,但在equals方法中使用了多个属性进行比较,这会导致不一致。例如:

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 这种形式,其中 @ 后面的数值为散列码的无符号十六进制表示

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
6、clone()

1. cloneable

clone() 是 Object 的 protected 方法,它不是 public,一个类不显式去重写 clone(),其它类就不能直接去调用该类实例的 clone() 方法

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() 得到以下实现:

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

public class CloneExample implements Cloneable {
    private int a;
    private int b;

    @Override
    public Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

那么clone()方法到底有什么作用呢?为什么我们要使用clone()方法呢

答:clone() 将一个对象的值复制到另一个对象。 clone() 方法节省了用于创建对象的精确副本的额外处理任务

正如您在下面的示例中看到的,两个引用变量具有相同的值。

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();
    }
} 

输出 :

101 amit
101 amit

如果我们通过 new 关键字创建另一个对象并将另一个对象的值分配给这个对象,则需要对该对象进行大量处理。因此,为了节省额外的处理任务,我们使用 clone() 方法。

为什么我们在 Java 中使用 clone() 方法?

2. 浅拷贝

拷贝对象和原始对象的引用类型引用同一个对象

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. 深拷贝

拷贝对象和原始对象的引用类型引用不同对象

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(),可以使用拷贝构造函数或者拷贝工厂来拷贝一个对象

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
7、switch

switch 语句是用于多路分支选择的一种控制结构,它根据一个变量的值选择执行不同的分支。每个分支通过 case 关键字定义,当变量的值与某个 case 标签匹配时,程序从该 case 标签开始执行,直到遇到 breakreturnthrow 等语句,或到达 switch 语句的末尾。

下面是 switch 语句的基本结构和工作原理

switch (expression) {
    case value1:
        // 代码块
        break;
    case value2:
        // 代码块
        break;
    // 可以有任意数量的 case 语句
    ...
    default:
        // 默认代码块
}
(1).工作原理
  1. 计算表达式:计算 switch 语句中的表达式 expression 的值。
  2. 匹配 case 标签:从上到下逐个匹配 case 标签的值,如果找到了匹配的 case,程序将执行对应的代码块。
  3. “Fall-through”机制:如果没有遇到 breakreturnthrow 等语句,程序会继续执行下一个 case 标签中的代码,直到遇到 breakreturnthrow 等语句或 switch 语句的末尾。
  4. 执行默认代码块:如果没有匹配到任何 case 标签,程序将执行 default 中的代码块(如果有 default 代码块)。
(2).示例代码
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;
    case 6:
        System.out.println("Saturday");
        break;
    case 7:
        System.out.println("Sunday");
        break;
    default:
        System.out.println("Invalid day");
}

在这个例子中,day 的值为 3,所以输出将是 Wednesday。当匹配到 case 3 时,程序执行 System.out.println("Wednesday");,然后遇到 break 语句,结束 switch 语句。

(3).fall-through

Java 中 switch 语句的“fall-through”特性。

switch 语句中,如果某个 case 标签下没有 breakreturnthrow 等语句,那么程序会继续执行下一个 case 标签的代码。

这使得多个 case 标签可以共享相同的代码块。

举例说明

在以下 case 标签都没有 break 语句:

case NO_ACCESS:
case THE_LIMIT:
case OVERSEAS_CUSTOMERS:
case PARAM_ERROR:
case SYSTEM_ERROR:
    throw new BusinessException();

这些 case 标签的处理逻辑都是抛出 BusinessException 异常。由于它们共享相同的处理逻辑,所以可以写在一起,这样可以避免代码重复。

具体的原理如下:

  1. 匹配 case 标签switch 语句会根据 edbRetCodeEnum 的值去匹配相应的 case 标签。
  2. 执行匹配到的 case:一旦匹配到某个 case 标签,如果没有 breakreturnthrow 等语句,程序会继续执行后面的代码。
  3. “Fall-through”机制:如果某个 case 标签下没有 break 语句,程序会继续执行后面的 case 标签中的代码,直到遇到 breakreturnthrow 等语句,或到达 switch 语句的末尾。

举个例子

int value = 2;
switch (value) {
    case 1:
        System.out.println("Case 1");
    case 2:
        System.out.println("Case 2");
    case 3:
        System.out.println("Case 3");
        break;
    default:
        System.out.println("Default case");
}

输出结果将是:

Case 2
Case 3
Default case

因为当 value 等于 2 时,程序匹配到 case 2,然后继续执行 case 3 以及 default 中的代码,直到遇到 break 语句。

在此代码中利用了这种==“fall-through”==机制,使得 NO_ACCESSTHE_LIMITOVERSEAS_CUSTOMERSPARAM_ERRORSYSTEM_ERROR 这些 case 标签都共享同一个 throw new BusinessException(...) 的处理逻辑。这样做不仅减少了重复代码,也使代码更加简洁和易于维护。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值