Thinking In Java第四版中文版(前12章)

第1章 对象入门

继承

基本语法

在Java中,使用extends关键字来实现继承。基本语法如下:
class 父类 {
}
 
class 子类 extends 父类 {
}

Java 不支持多继承,但支持多重继承。

继承的特性

+ 子类拥有父类 非 private 的属性、方法。 + 子类可以拥有自己的属性和方法,即子类可以对父类进行扩展。 + 子类可以用自己的方式实现父类的方法。

类的单一继承->extends

在 Java 中,类的继承是单一继承,也就是说,一个子类只能拥有一个父类,所以 extends 只能继承一个类。
public class Animal { 
    private String name;   
    private int id; 
    public Animal(String myName, int myid) { 
        //初始化属性值
    } 
    public void eat() {  //吃东西方法的具体实现  } 
    public void sleep() { //睡觉方法的具体实现  } 
} 
 
public class Penguin  extends  Animal{ 
}

接口的伪多继承->implements

接口是完全抽象的,它不能被实例化。不能创建一个接口类型的对象,但可以创建实现了该接口的类的对象。

使用 implements 关键字可以变相的使java具有多继承的特性,使用范围为类继承接口的情况,可以同时继承多个接口(接口跟接口之间采用逗号分隔)。

public interface A {
    public void eat();
    public void sleep();
}
 
public interface B {
    public void show();
}
 
public class C implements A,B {
}

super 与this

super关键字:我们可以通过super关键字来实现对父类成员的访问,用来引用当前对象的父类。

this关键字:指向自己的引用。

class Animal {
  void eat() {
    System.out.println("animal : eat");
  }
}
 
class Dog extends Animal {
  void eat() {
    System.out.println("dog : eat");
  }
  void eatTest() {
    this.eat();   // this 调用自己的方法
    super.eat();  // super 调用父类方法
  }
}
 
public class Test {
  public static void main(String[] args) {
    Animal a = new Animal();
    a.eat();
    Dog d = new Dog();
    d.eatTest();
  }
}
、、、、、输出
animal : eat
dog : eat
animal : eat

final关键字

final的关键作用是表示“最终的”或“不可变的”

1. 应用于变量

当final修饰一个变量时,该变量的值一旦被初始化之后就不能被改变。这意味着final变量成为了一个常量。

基本数据类型:final int MAX_VALUE = 100; 这里MAX_VALUE是一个常量,其值不能被改变。

对象引用:final List NAMES = new ArrayList<>(); 这里NAMES引用不能被改变,但NAMES指向的ArrayList对象的内容是可以被修改的(除非ArrayList也被声明为不可变的)。

2. 应用于方法

当final修饰一个方法时,这个方法不能在子类中被重写。

3. 应用于类

当final修饰一个类时,这个类不能被继承。

final变量必须被初始化。对于成员变量,可以在声明时初始化,或者在构造函数中初始化(对于实例变量);对于局部变量,必须在声明时初始化。

final方法不能被子类重写,但可以被重载。

final类不能被继承,但可以实现接口。(即使一个类被声明为 final,它仍然可以声明自己实现了某个接口,并提供该接口中所有抽象方法的具体实现。)

继承的特点

1. ** 单一继承** :Java只支持单一继承,即一个类只能有一个直接父类。 2. ** 多层继承** :虽然Java不支持多重继承(即一个类继承多个类),但可以通过继承的传递性实现多层继承。 3. ** 继承方法的重写** :子类可以重写(Override)从父类继承的方法,即提供特定于子类的实现。 4. ** 继承构造方法** : 子类不能继承父类的构造方法 ,但子类构造方法可以通过super关键字调用父类的构造方法。 5. ** super关键字** :super关键字用于引用当前对象的直接父类。

Java 重写(Override)与重载(Overload)

重写(Override)

当子类中的方法与父类中的某个方法具有相同的 名称、返回类型以及参数列表 时,我们说子类重写了父类的方法。重写的方法可以修改父类方法的行为。

特点

  • 方法名、参数列表必须相同。
  • 返回类型必须相同或者是子类型(Java 5及以后版本支持协变返回类型)。
  • 访问权限不能比父类中被重写的方法的访问权限更低(例如,如果父类中的方法是protected,则子类中的重写方法不能是private)。
  • 抛出的异常类型应该比父类中被重写的方法抛出的异常类型更窄或相同(Java 7及以后版本支持更灵活的异常处理规则)。
  • 声明为 final 的方法不能被重写。
  • 声明为 static 的方法不能被重写,但是能够被再次声明。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
  • 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
  • 构造方法不能被重写。
  • 如果不能继承一个类,则不能重写该类的方法。

用途

  • 实现多态性。
  • 修改继承来的方法的行为。

方法的重写规则

  • 参数列表与被重写方法的参数列表必须完全相同。
  • 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5 及更早版本返回类型要一样,java7 及更高版本可以不同)。
  • 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为 protected。
  • 父类的成员方法只能被它的子类重写。
  • 声明为 final 的方法不能被重写。
  • 声明为 static 的方法不能被重写,但是能够被再次声明。
  • 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法。
  • 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。

重载(Overload)

重载是在同一个类中,允许存在多个同名方法,只要它们的参数列表不同即可。参数列表的不同包括参数的类型、个数或顺序的不同。

特点

  • 方法名必须相同。
  • 参数列表必须不同(参数类型、个数或顺序至少有一项不同)。
  • 返回类型可以相同也可以不同。
  • 访问修饰符和抛出的异常类型没有限制。

用途

  • 提供了同一个类中方法的多样性。
  • 使得程序更加灵活,易于阅读和维护。

多态

当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。

多态的好处:可以使程序有良好的扩展,并可以对所有类的对象进行通用处理。

1. 编译时多态(方法重载)

编译时多态主要通过方法重载(Overloading)实现。方法重载指的是在同一个类中,允许存在一个以上的同名方法,只要它们的参数个数或者参数类型不同即可。方法重载与多态性的关系主要体现在它允许我们以统一的方式调用不同版本的同名方法。

2. 运行时多态(方法覆盖)

运行时多态主要通过方法覆盖(Overriding)和接口实现来体现。方法覆盖发生在有继承关系的两个类之间,子类可以定义一个与父类同名且有相同参数列表的方法,通过子类对象调用该方法时,将执行子类中的定义,这就是多态性。

在这个例子中,b是Animal类型的引用,但是它指向了一个Dog对象。当调用b.eat()时,由于Dog类覆盖了Animal类的eat方法,所以实际上执行的是Dog类中的eat方法,这体现了多态性。

实现多态的关键

+ ** 继承** :多态性的前提是继承。 + ** 方法的覆盖** :子类对父类方法的覆盖是实现多态性的关键。 + ** 向上转型** :父类引用指向子类对象,这种转型是安全的,因为子类对象拥有父类对象所有的属性和方法。 + ** 动态绑定** :Java虚拟机在运行时才确定要调用的方法,这是通过方法的动态绑定实现的。

抽象类

Java中的抽象类是一种特殊的类,它不能被实例化(即不能创建该类的对象)。抽象类主要用于定义一组方法,这些方法的具体实现由它的子类来完成。

抽象类可以定义一些抽象方法,这些方法没有具体的实现,子类必须实现这些抽象方法才能被实例化。这种机制确保了子类在继承抽象类时,必须实现特定的行为或功能,从而保证了子类的完整性和一致性

定义抽象类

在Java中,使用abstract关键字来定义一个抽象类。抽象类中可以包含抽象方法(即没有具体实现的方法)和非抽象方法(即包含实现体的方法)。 如果类中包含至少一个抽象方法,那么这个类就必须被声明为抽象类 。但是,即使类中没有抽象方法,也可以将其声明为抽象类,这样做主要是为了防止类的实例化。

抽象方法的定义

抽象方法也是在方法声明时使用abstract关键字,并且它们没有方法体。抽象方法的具体实现由子类负责提供。
// 定义一个抽象类  
abstract class Animal {  
    // 抽象方法  
    abstract void eat();  
  
    // 非抽象方法  
    void sleep() {  
        System.out.println("Animal is sleeping.");  
    }  
}  
  
// 继承抽象类的子类  
class Dog extends Animal {  
    // 必须实现父类中的抽象方法  
    @Override  
    void eat() {  
        System.out.println("Dog is eating.");  
    }  
}  
  
public class TestAbstractClass {  
    public static void main(String[] args) {  
        // Animal a = new Animal(); // 错误,因为Animal是抽象类  
        Dog d = new Dog();  
        d.eat();  
        d.sleep();  
    }  
}

接口

接口与类的区别:

+ 接口不能用于实例化对象。 + 接口没有构造方法。 + 接口中所有的方法必须是抽象方法,Java 8 之后 接口中可以使用 default 关键字修饰的非抽象方法。 + 接口不能包含成员变量,除了 static 和 final 变量。 + 接口不是被类继承了,而是要被类实现。 + 接口支持多继承。 + 接口中的方法会被隐式的指定为 ** public abstract** + 接口中的变量会被隐式的指定为 ** public static final** 变量
interface Animal {  
    // 常量定义  
    int AGE = 5; //此为常量,变量是不赋值
  
    // 抽象方法  
    void eat();  
  
    // Java 8 引入的默认方法  
    default void sleep() {  
        System.out.println("Animal is sleeping.");  
    }  
  
    // Java 8 引入的静态方法  
    static void showInfo() {  
        System.out.println("This is an interface method.");  
    }  
}
class Dog implements Animal {  
    @Override  
    public void eat() {  
        System.out.println("Dog is eating.");  
    }  
    // 可以选择不实现默认方法,直接使用接口中定义的默认实现  
    // 但如果子类需要修改默认方法的行为,则可以在子类中提供该方法的具体实现  
}

接口的多继承

在Java中,类的多继承是不合法,但接口允许多继承。

在接口的多继承中extends关键字只需要使用一次,在其后跟着继承接口。 如下所示:

public interface Hockey extends Sports, Event

第 2 章 一切都是对象

static

用于声明属于类本身而不是属于类对象的字段(变量)、方法或代码块。这意味着无论创建了多少个类的对象,静态成员都只有一份拷贝,并且被所有对象共享。

1. 静态变量(Static Variables)

静态变量属于类,而不是类的任何特定对象。无论创建了多少个对象,静态变量都只有一份拷贝。静态变量在类加载到JVM时初始化,并且可以被类的所有实例共享和访问。
public class MyClass {  
    static int staticVar = 42; // 静态变量  
  
    public static void main(String[] args) {  
        MyClass obj1 = new MyClass();  
        MyClass obj2 = new MyClass();  
  
        // 两个对象共享同一个静态变量  
        System.out.println(obj1.staticVar); // 输出 42  
        System.out.println(obj2.staticVar); // 输出 42  
  
        // 直接通过类名访问静态变量  
        System.out.println(MyClass.staticVar); // 输出 42  
    }  
}

2. 静态方法(Static Methods)

静态方法属于类,而不是类的实例 。静态方法不能访问类的非静态成员(除非通过对象实例来访问),因为非静态成员属于类的具体对象。 静态方法可以通过类名直接调用,而无需创建类的实例。
public class MyClass {  
    static void staticMethod() {  
        System.out.println("这是一个静态方法");  
    }  
  
    public static void main(String[] args) {  
        // 直接通过类名调用静态方法  
        MyClass.staticMethod(); // 输出:这是一个静态方法  
    }  
}

2.2.1 保存到什么地方

1. ** 寄存器(Registers)** : + 访问速度非常快。然而,寄存器的数量有限,且完全由编译器自动管理,程序员无法直接控制。 2. ** 堆栈(Stack)** : + 堆栈是一种特别快、特别有效的数据保存 方式,仅次于寄存器 。 + Java中的 基本数据类型变量 (如int、float等)和 对象引用 (句柄,而非对象本身)通常存储在堆栈中。堆栈的分配和释放非常快,但大小通常有限。 3. ** 堆(Heap)** : + 堆是Java用来 存储对象实例 的内存区域。与堆栈不同,堆的大小不固定,且对象在堆中的生命周期由程序员控制(通过创建和销毁对象)。 4. ** 静态存储(Static Storage)** : + 可用static 关键字指出一个对象的特定元素是静态的。 + 静态存储区域用于存储类的静态变量和静态方法。这些变量和方法在程序运行期间始终存在,并且对所有对象实例共享。 5. ** 常数存储(Constant Storage)** : + 常数值(如字符串常量、基本类型的常量值等)通常存储在程序的代码段或数据段中,具体取决于编译器的实现。这些值在编译时就已确定,并在程序执行期间保持不变。 6. ** 非RAM存储(Non-RAM Storage)** : + 非RAM存储包括磁盘、文件、数据库等外部存储设备。Java程序可以通过文件IO、数据库连接等方式与这些存储设备交互,以实现数据的持久化存储。

javadoc

![](https://img-blog.csdnimg.cn/img_convert/7b1438161068f5d3ed5a5aafb2296334.png)
//接口定义
interface Animal {
    // 常量定义
    int AGE = 5; //此为常量,变量是不赋值

    // 抽象方法
    void eat();

    // Java 8 引入的默认方法
    default void sleep() {
        System.out.println("Animal is sleeping.");
    }

    // Java 8 引入的静态方法
    static void showInfo() {
        System.out.println("This is an interface method.");
    }
}
/**
 * 1. @ClassDescription:
 * 2. @author: Xingshihao
 * 3. @date: 2024年07月09日 16:45
 */
public class Dog implements Animal{
    @Override
    public void eat() {
        System.out.println("Dog is eating.");
    }
/*     可以选择不实现默认方法,直接使用接口中定义的默认实现
     但如果子类需要修改默认方法的行为,则可以在子类中提供该方法的具体实现*/
}
public class Main {
    public static void main(String[] args) {
       Dog dog = new Dog();
       dog.eat();
       dog.sleep();
       Animal.showInfo();
    }
}

生成javadoc

解决中文生成javadoc报错

-encoding UTF-8 -charset UTF-8

第 3 章 控制程序流程

赋值

```java class Number {int i;} public class Assignment { public static void main(String[] args) { Number n1 = new Number(); Number n2 = new Number(); n1.i = 9; n2.i = 47; System.out.println("1: n1.i: " + n1.i +", n2.i: " + n2.i); n1 = n2; System.out.println("2: n1.i: " + n1.i +", n2.i: " + n2.i); n1.i = 27; System.out.println("3: n1.i: " + n1.i +", n2.i: " + n2.i);}} —————————————————————— 1: n1.i: 9, n2.i: 47 2: n1.i: 47, n2.i: 47 3: n1.i: 27, n2.i: 27 ```

n1 = n2; 这条语句是一个赋值操作,但它并不是在赋值对象的“内容”或“值”,而是在赋值对象的“引用”。

当执行 n1 = n2; 时,你实际上是在告诉 n1 现在也指向 n2 所指向的对象。这意味着 n1 和 n2 现在都持有对同一个对象的引用。因此,如果你通过 n1 修改了这个对象的某个字段(比如 n1.i = 27;),那么通过 n2 访问这个字段时也会看到相同的修改,因为 n1 和 n2 指向的是同一个对象。

当执行 n1 = n2; 之后,n1 原本所引用的对象(即之前通过 new Number(); 创建的那个对象)并没有被删除或销毁,但它变得“不可达”(unreachable)了。在Java中,垃圾回收器(Garbage Collector, GC)负责回收那些不再被任何引用变量所引用的对象,以释放它们所占用的内存空间。

这是Java中对象引用的基本行为之一,也是与基本数据类型(如int, double等)的主要区别之一。基本数据类型的变量存储的是值本身,而对象类型的变量(引用变量)存储的是对对象的引用(或内存地址)。

短路

在某些条件下,逻辑表达式的求值过程会在能够确定整个表达式的结果时提前停止,而不是对表达式中的所有操作数都进行求值。

短路 AND (**<font style="color:#000000;">&&</font>**)

对于逻辑 AND 运算符(<font style="color:#000000;">&&</font>),如果第一个操作数(即左侧的表达式)的结果为 <font style="color:#000000;">false</font>,那么整个逻辑表达式的结果就已经确定为 <font style="color:#000000;">false</font>,因为不论第二个操作数(即右侧的表达式)的结果是什么,整个表达式的结果都不可能是 <font style="color:#000000;">true</font>。因此,在这种情况下,第二个操作数不会被求值,这就是所谓的“短路”。

短路 OR (**<font style="color:#000000;">||</font>**)

对于逻辑 OR 运算符(<font style="color:#000000;">||</font>),如果第一个操作数(即左侧的表达式)的结果为 <font style="color:#000000;">true</font>,那么整个逻辑表达式的结果就已经确定为 <font style="color:#000000;">true</font>,因为不论第二个操作数(即右侧的表达式)的结果是什么,整个表达式的结果都已经是 <font style="color:#000000;">true</font>。因此,在这种情况下,第二个操作数同样不会被求值,这也是一种“短路”。

按位运算符

** 1. 按位AND(&)**

操作:对两个数的二进制表示进行逐位AND操作。

结果:如果两个相应的位都为1,则结果位为1;否则为0。

用途:常用于清零、取两个数的公共位等。

2. 按位OR(|)

操作:对两个数的二进制表示进行逐位OR操作。

结果:如果两个相应的位中至少有一个为1,则结果位为1;如果两个位都是0,则结果位为0。

用途:常用于设置特定位、合并两个数的不同位等。

3. 按位XOR(^)

操作:对两个数的二进制表示进行逐位XOR操作。

结果:如果两个相应的位不同,则结果位为1;如果相同,则结果位为0。

用途:常用于切换特定位、检查两个数是否在某个位上不同等。

4. 按位NOT(~)

操作:对单个数的二进制表示进行逐位NOT操作。

结果:将数的所有位取反(0变1,1变0)。

用途:通常用于生成一个数的二进制补码(在某些上下文中),或者快速地反转所有位。

5. 复合赋值运算符

形式:&=、|=、^=

用途:将按位运算的结果直接赋值给左侧的变量,避免了单独进行运算和赋值的步骤。

关于布尔类型与按位运算符

对于布尔类型,可以使用&、|和^进行逻辑AND、OR和XOR操作,但这些操作实际上是逻辑运算,不是按位运算。这是因为Java不允许对布尔类型进行按位NOT(~)操作,以避免与逻辑NOT(!)混淆。

布尔类型的逻辑运算与按位运算在结果上可能相同,但它们的处理方式和用途是不同的。逻辑运算会进行短路评估(即,如果第一个操作数已经确定了整个表达式的结果,则不会评估第二个操作数),而按位运算总是评估两个操作数。

移位运算符

1. 左移运算符(`<<`)

左移运算符将数的二进制表示向左移动指定的位数,左边操作数的左边界之外丢弃,右边界外补0。
int a = 4; // 二进制表示为 0000 0100  
int b = a << 2; // 结果为 16,二进制表示为 0001 0000

2. 有符号右移运算符(`>>`)

有符号右移运算符将数的二进制表示向右移动指定的位数,左边界外补符号位(即最左边的位,0表示正数,1表示负数)。
int c = -8; // 假设是32位int,二进制表示为 1111 1111 1111 1000(补码表示)  
int d = c >> 2; // 结果为 -2,二进制表示为 1111 1111 1111 1110(补码表示)

3. 无符号右移运算符(`>>>`)

无符号右移运算符与有符号右移类似,但它总是用0来填充左边界之外的位,无论操作数的符号位是什么。这意味着无论操作数是正是负,结果都将被视为无符号数。
int e = -8; // 假设是32位int,二进制表示为 1111 1111 1111 1000(补码表示)  
int f = e >>> 2; // 结果为 1073741822,二进制表示为 0011 1111 1111 1110(无符号解释)

在无符号右移的情况下,如果操作数是<font style="color:#000000;">byte</font><font style="color:#000000;">short</font>类型,它们会被提升为<font style="color:#000000;">int</font>类型,并且整个<font style="color:#000000;">int</font>值(包括符号位)都会参与右移操作。但是,由于结果被视为无符号数,因此即使原始值是负数,结果也不会是负数(在Java的<font style="color:#000000;">int</font>类型范围内)。

三元if-else运算符

:::color2 条件表达式 ? 表达式1 : 表达式2;

:::

如果条件表达式的结果为<font style="color:#000000;">true</font>,则整个三元运算符的值为<font style="color:#000000;">表达式1</font>的结果;如果条件表达式的结果为<font style="color:#000000;">false</font>,则整个三元运算符的值为<font style="color:#000000;">表达式2</font>的结果。

强制类型转换

```java double d = 3.14; int i = (int)d; // 将double类型的d转换为int类型,并赋值给i ```

这种转换可能会导致数据丢失,因为<font style="color:#000000;">int</font>类型无法存储<font style="color:#000000;">double</font>类型中的小数部分。

  1. 缩小转换:从表示范围较大的类型转换为表示范围较小的类型(如从<font style="color:#000000;">double</font><font style="color:#000000;">int</font>,或从<font style="color:#000000;">float</font><font style="color:#000000;">byte</font>)时,称为缩小转换。这种转换可能会导致数据丢失或精度降低,因此Java要求必须显式地进行这种转换。
  2. 放大转换:从表示范围较小的类型转换为表示范围较大的类型(如从<font style="color:#000000;">byte</font><font style="color:#000000;">int</font>,或从<font style="color:#000000;">int</font><font style="color:#000000;">long</font>)时,称为放大转换。这种转换是安全的,因为新类型能够容纳原类型的所有值,因此Java允许自动进行这种转换,无需显式造型。
  3. 布尔类型不能转换:布尔类型(<font style="color:#000000;">boolean</font>)不能被转换为其他任何类型,也不能从其他类型造型为布尔类型。

switch

```java switch (expression) { case value1: // 当expression的值等于value1时执行的代码 break; case value2: // 当expression的值等于value2时执行的代码 break; // 可以有更多的case... default: // 当expression的值不等于任何case的值时执行的代码 } ```

第4章 初始化和清除

方法过载

方法过载(Method Overloading)是一种允许在同一个类中定义多个同名方法,但它们的参数列表(参数的类型、数量或顺序)必须不同的特性。
  • 方法名必须相同。
  • 参数列表必须不同(参数的类型、数量或顺序至少有一项不同)。
  • 方法的返回类型可以相同也可以不同,但它不是区分方法重载的依据。
  • 访问修饰符、是否抛出异常等也不作为方法重载的依据。

构建器(构造方法)过载

public class Person {  
    private String name;  
    private int age;  
  
    // 默认构造方法  
    public Person() {  
        this.name = "Unknown";  
        this.age = 0;  
    }  
  
    // 带名字和年龄参数的构造方法  
    public Person(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }  
  
    // 其他方法...  
}  
  
// 使用  
Person p1 = new Person(); // 调用默认构造方法  
Person p2 = new Person("Alice", 30); // 调用带参数的构造方法

普通方法过载

Java也允许普通方法的过载。这意味着在同一个类中,可以定义多个同名但参数列表不同的方法。

public class Person {  
    // ...之前的代码  
  
    // 根据名字查找Person  
    public static Person find(String name) {  
        // 实现逻辑  
        return new Person(name, 0); // 示例返回  
    }  
  
    // 根据ID查找Person  
    public static Person find(int id) {  
        // 实现逻辑  
        return new Person("Unknown ID: " + id, 0); // 示例返回  
    }  
  
    // ...其他方法  
}  
// 使用  
Person p3 = Person.find("Bob"); // 调用find(String)  
Person p4 = Person.find(123); // 调用find(int)

主类型的过载

** 参数类型“小于”期望的方法参数类型**

当传递的参数类型“小于”期望的方法参数类型时,Java会尝试进行自动类型转换(也称为隐式类型转换)。这种转换通常是从一个较小的数据类型(如byte、short、char)转换到一个较大的数据类型(如int、long、float、double)。但是,对于char类型来说,情况有点特殊,因为虽然它可以隐式地转换为int、long、float或double,但在方法重载的上下文中,如果找不到与char完全匹配的重载方法,则通常会首先尝试将其转换为int(因为int是与char在大小和用法上最接近的整数类型)。

参数类型“大于”期望的方法参数类型

当传递的参数类型“大于”期望的方法参数类型时,Java不会自动进行这种“反向”的转换(即从较大的类型转换为较小的类型)。这种转换被称为缩小转换(Narrowing Conversion),并且可能会导致数据丢失或精度下降。因此,如果你想要将一个较大的数据类型(如double)传递给一个期望较小数据类型(如int)参数的方法,你必须显式地进行类型转换。

Java 的 <font style="color:#000000;">finalize()</font> 方法是 <font style="color:#000000;">Object</font> 类的一个受保护方法(<font style="color:#000000;">protected</font>),它允许开发者在对象被垃圾收集器销毁之前执行清理操作。然而,随着 Java 的发展,<font style="color:#000000;">finalize()</font> 方法的使用已经变得非常不推荐,主要是因为它的不确定性和潜在的性能问题。不过,了解它的工作原理和限制仍然是有价值的。

finalize()方法

当垃圾收集器(GC)确定某个对象没有引用指向它时,即该对象不再被应用程序使用,GC 会准备回收该对象占用的内存。在回收之前,如果对象所属的类覆盖了 ` finalize()` 方法,GC 会将该对象的引用传递给 ` finalize()` 方法,并暂时将其从回收队列中移除。这样,` finalize()` 方法就有机会执行清理操作,比如释放非 Java 堆内存资源、关闭文件或网络连接等。

然而,重要的是要明白,即使 <font style="color:#000000;">finalize()</font> 被调用,对象的内存也不会立即被回收。相反,对象会被放入一个“终结队列”(finalization queue)中,等待下一次垃圾收集周期。只有在下一次垃圾收集时,如果 GC 发现该对象仍然没有被任何引用指向,它才会真正回收该对象的内存。

潜在问题

  1. 不确定性:由于垃圾收集的时间和 <font style="color:#000000;">finalize()</font> 方法的调用时机都是不确定的,因此依赖 <font style="color:#000000;">finalize()</font> 进行资源清理可能会导致资源泄露或延迟释放。
  2. 性能开销<font style="color:#000000;">finalize()</font> 方法的调用会增加垃圾收集器的负担,因为 GC 需要跟踪哪些对象覆盖了 <font style="color:#000000;">finalize()</font> 并调用它们。此外,<font style="color:#000000;">finalize()</font> 方法中的代码如果执行时间过长,还会影响应用程序的响应性和吞吐量。
  3. 复活对象:在 <font style="color:#000000;">finalize()</font> 方法中,对象可以通过重新获得一个引用(如赋值给某个静态变量)来“复活”自己。这会使对象在后续的垃圾收集周期中再次成为候选对象,但 <font style="color:#000000;">finalize()</font> 方法只会被调用一次。
  4. 替代方案:现代 Java 应用程序倾向于使用更明确的资源管理策略,如 <font style="color:#000000;">try-with-resources</font> 语句(自动管理资源,如文件、数据库连接等)或显式的关闭和清理方法(如 <font style="color:#000000;">Closeable</font> 接口中的 <font style="color:#000000;">close()</font> 方法)。

初始化顺序

对于** 实例变量(即非静态变量)** ,它们的初始化顺序是按照它们在类中被声明的顺序进行的,而不是按照它们在构造函数或任何方法中被引用的顺序。

静态变量的初始化发生在类被加载到JVM时,而不是在创建类的实例时。静态变量的初始化顺序也是按照它们在类定义中出现的顺序进行的。

第5 章 隐藏实施过程

5.1 包:库单元

** package**
  1. 把功能相似或相关的类或接口组织在同一个包中,方便类的查找和使用
  2. 如同文件夹一样,包也采用了树形目录的存储方式。同一个包中的类名字是不同的,不同的包中的类的名字是可以相同的,当同时调用两个不同包中相同类名的类时,应该加上包名加以区别。因此,包可以避免名字冲突。
  3. 包也限定了访问权限,拥有包访问权限的类才能访问某个包中的类。
// 声明该类位于com.mycompany.library包中,存储路径为com/mycompany/library/Example.java
package com.mycompany.library;  
  
public class Example {  
    // 类体...  
    public void doSomething() {  
        // 方法实现...  
    }  
}

import

另一个开发者想要在他们的项目中使用<font style="color:#000000;">com.mycompany.library</font>包中的<font style="color:#000000;">Example</font>类。他们可以通过<font style="color:#000000;">import</font>关键字来导入这个类。

// 导入com.mycompany.library包中的Example类  
import com.mycompany.library.Example;  
  
public class Main {  
    public static void main(String[] args) {  
        // 创建Example类的实例并调用其方法  
        Example example = new Example();  
        example.doSomething();  
    }  
}

解决命名冲突

如果两个不同的库或项目中定义了相同名称的类,并且你想要在同一个Java文件中使用这两个类,可以通过指定它们的完整包名来解决命名冲突。

// 假设有两个不同的包都定义了名为Utils的类  
// com.library1.Utils 和 com.library2.Utils  
import com.library1.Utils as Utils1;  
import com.library2.Utils as Utils2;  
  
public class ConflictExample {  
    public static void main(String[] args) {  
        // 使用别名来区分两个Utils类  
        Utils1 utils1 = new Utils1();  
        Utils2 utils2 = new Utils2();  
        // 调用各自的方法...  
    }  
}

Java 包(package) | 菜鸟教程

5.1.3 利用导入改变行为

** Java的断言**

Java有一个内置的断言机制,它使用<font style="color:#000000;">assert</font>关键字。但是,断言主要用于调试目的,而不是作为程序逻辑的一部分。断言在默认情况下是禁用的,并且需要在运行时通过JVM参数来启用。

使用包和导入来模拟条件编译

假设我们有两个包:

  • <font style="color:#000000;">com.bruceeckel.tools.debug</font>
  • <font style="color:#000000;">com.bruceeckel.tools</font>

<font style="color:#000000;">com.bruceeckel.tools.debug</font>包中,有一个<font style="color:#000000;">Assert</font>类,它包含了很多用于调试的断言方法,比如<font style="color:#000000;">assertTrue</font>,当断言失败时会打印出错误信息。在<font style="color:#000000;">com.bruceeckel.tools</font>包中,也有一个<font style="color:#000000;">Assert</font>类,但这个类可能不包含任何实际的断言逻辑,或者只是简单地忽略断言,不执行任何操作。

在开发过程中,可以在代码中导入<font style="color:#000000;">com.bruceeckel.tools.debug.Assert</font>来使用调试版本的<font style="color:#000000;">Assert</font>类。当运行程序时,如果断言失败,会看到详细的错误信息。但是,当准备发布你的程序时,只需要更改导入语句,改为导入<font style="color:#000000;">com.bruceeckel.tools.Assert</font>。由于这个版本的<font style="color:#000000;">Assert</font>类不包含实际的断言逻辑,所以断言调用将不会有任何效果,程序将不包含任何调试信息。

// 开发过程中  
import com.bruceeckel.tools.debug.Assert;  
public class MyApp {  
    public static void main(String[] args) {  
        Assert.assertTrue("This should be true", true); // 如果为false,将打印错误信息  
        // 其他代码...  
    }  
}  
  
// 发布时  
// 只需要更改导入语句  
import com.bruceeckel.tools.Assert;  
public class MyApp {  
    public static void main(String[] args) {  
        Assert.assertTrue("This should be true", true); // 但这个版本可能不执行任何操作  
        // 其他代码...  
    }  
}

5.2 Java 访问指示符

![](https://img-blog.csdnimg.cn/img_convert/4166764c5355d708c3d695f7802107d8.png)

<font style="color:#000000;">private</font>

  • 访问范围:只能被定义它们的类内部访问。
  • 用途:用于隐藏类的内部实现细节,提高封装性。

默认(Friendly)

  • 访问范围:在同一包(package)内的任何类都可以访问。
  • 用途:用于包级别的封装,通常用于包内部的类之间的交互。

<font style="color:#000000;">protected</font>

  • 访问范围:在同一个包内的任何类都可以访问,以及不同包中的子类也可以访问。
  • 用途:用于实现继承时的封装,保护子类可以访问但又不希望外部类访问的成员。
  • 子类与基类在同一包中:被声明为 protected 的变量、方法和构造器能被同一个包中的任何其他类访问;
  • 子类与基类不在同一包中:那么在子类中,子类实例可以访问其从基类继承而来的 protected 方法,而不能访问基类实例的protected方法。
// 父类  
class MyBaseClass {  
    protected int myNumber = 10; // 子类可以访问  
}  
 
// 子类,即使在不同包中也可以访问protected成员  
class MySubClass extends MyBaseClass {  
    void accessMyNumber() {  
        System.out.println(myNumber); // 可以访问  
    }  
}

<font style="color:#000000;">public</font>

  • 访问范围:可以被任何类访问,无论它们是否在同一个包中。
  • 用途:用于公开类的接口,使得其他任何类都可以使用这些接口。

访问控制和继承

  • 父类中声明为 public 的方法在子类中也必须为 public。
  • 父类中声明为 protected 的方法在子类中要么声明为 protected,要么声明为 public,不能声明为 private。
  • 父类中声明为 private 的方法,不能够被子类继承。

5.4 类访问

在Java中,类的访问控制是一个重要的概念,它决定了哪些类可以被其他类访问和使用。

类的访问控制

public 类:如果一个类被声明为public,那么它可以被任何其他类访问,只要它们能够访问到定义该类的包。通常,

默认(包级私有)类:如果一个类没有使用任何访问修饰符(即它是默认的),那么这个类只能被同一个包内的其他类访问。这种情况下,类的可见性被限制在了定义它的包内。

类的封装与隐藏

有时候,可能不希望类的某些实现细节被外部类直接访问或修改。为了实现这一点,可以将所有构建器(构造函数)设为private,从而防止外部类直接创建类的实例。

第 6 章 类再生

** 合成**

假设我们有一个表示汽车(Car)的类和一个表示引擎(Engine)的类。引擎是汽车的一个组件,因此我们可以使用合成的方式将引擎对象嵌入到汽车对象中。

// 引擎类  
pubilc class Engine
    // 省略方法和属性  
}  
  
// 汽车类  
public class Car {  
    private Engine engine;  
  
    public Car(Engine engine) {  
        this.engine = engine;  
    }  
    // 省略其他方法和属性  
}  
  
public class TestComposition {  
    public static void main(String[] args) {  
        Engine engine = new Engine("V8", 500);  
        Car car = new Car(engine);    
    }  
}

继承

假设我们有一个表示动物的基类(Animal)和一个表示狗的子类(Dog)。狗是动物的一种特殊类型,因此我们可以使用继承的方式让狗类继承动物类的属性和方法。

// 动物基类  
public class Animal {   
    }  
    // 省略其他方法和属性  
}  
  
// 狗子类  
public class Dog extends Animal {  
}  

6.2.1 初始化基础类

在Java中,创建一个子类的实例时,实际上也创建了一个父类的实例(即父类的“子对象”),这个父类实例被封装在子类实例中。子类的构造函数可以通过使用` super()` 关键字显式地调用父类的构造函数,如果没有显式调用,Java编译器会自动插入一个对父类无参构造函数的调用,如果父类没有无参构造函数,而子类构造函数又没有显式调用父类的其他构造函数,则会导致编译错误。
// 基础类  
class Animal {  
    String name;  
    Animal(String name) {  
        this.name = name;  
    }  
    void eat() {  
        System.out.println(name + " is eating.");  
    }  
}  
  
// 第一层派生类  
class Dog extends Animal {  
    int age;  
    Dog(String name, int age) {  
        // 显式调用父类的构造函数  
        super(name);  
        this.age = age;  
    }  
    void bark() {  
        System.out.println(name + " is barking.");  
    }  
}  
  
// 第二层派生类  
class GoldenRetriever extends Dog {  
    boolean isTrained;  
    GoldenRetriever(String name, int age, boolean isTrained) {  
        // 调用父类Dog的构造函数,而Dog的构造函数会进一步调用Animal的构造函数  
        super(name, age);  
        this.isTrained = isTrained;  
    }  
    void fetch() {  
        System.out.println(name + " is fetching.");  
    }  
}  
  
// 测试类  
public class InheritanceDemo {  
    public static void main(String[] args) {  
        GoldenRetriever myDog = new GoldenRetriever("Buddy", 5, true);  
        myDog.eat(); // 继承自Animal  
        myDog.bark(); // 继承自Dog  
        myDog.fetch(); // GoldenRetriever特有的方法  
    }  
}

6.3.1 确保正确的清除

** 1. 垃圾收集器的工作机制**

Java的垃圾收集器(Garbage Collector, GC)会自动回收那些不再被应用程序中的任何部分引用的对象所占用的内存。程序员不需要(也不应该)显式地销毁对象。然而,GC的启动时间、频率和回收对象的具体顺序都是不确定的,这是由JVM的垃圾收集算法和当前系统的内存使用情况决定的。

2. finalize() 方法

从Java 9开始,<font style="color:#000000;">finalize()</font>方法已被标记为<font style="color:#000000;">@Deprecated</font>(已弃用)。

3. 显式清除资源

当Java对象持有非内存资源(如文件句柄、网络连接、数据库连接等)时,需要在对象不再需要时显式地释放这些资源。这通常通过在类中提供显式的关闭或清理方法来实现,而不是依赖垃圾收集器。

例如,可以使用try-finally语句块来确保即使在发生异常的情况下也能释放资源:

try {  
    // 使用资源  
} finally {  
    // 释放资源  
    if (resource != null) {  
        resource.close();  
    }  
}

4. 清理方法的调用顺序

当子类对象需要清理时,如果它持有对基础类对象或成员对象的引用,并且这些对象也需要被清理,那么应该首先在子类的清理方法中完成与子类相关的清理工作,然后调用基础类的清理方法(如果有的话)。这样可以确保资源被正确且有序地释放。

6.5 protected

** 包内访问** :与默认(也称为包级私有)访问修饰符类似,protected 成员在同一包内的其他类中是可访问的。

子类访问:与私有(private)和默认访问修饰符不同,protected 成员还可以被其子类访问,无论子类位于哪个包中。

6.7 上溯造型

在Java中,"上溯造型"(Upcasting)是一种类型转换的术语,它指的是将子类的引用类型转换为父类的引用类型。这种转换是自动的,安全的,因为父类的引用可以指向子类对象,而无需显式地执行类型转换。这是面向对象编程中多态性的一个表现。

上溯造型示例

class Animal {  
    void eat() {  
        System.out.println("This animal eats food.");  
    }  
}  
class Dog extends Animal {  
    void bark() {  
        System.out.println("Woof!");  
    }  
}  
public class Test {  
    public static void main(String[] args) {  
        Dog myDog = new Dog(); 
        // 上溯造型:将Dog的引用转换为Animal的引用  
        Animal myAnimal = myDog;      
        // 使用Animal的引用调用eat方法,这是合法的  
        myAnimal.eat();   
        // 如果尝试使用Animal的引用调用Dog特有的bark方法,这将导致编译错误  
        // myAnimal.bark(); // 编译错误  
    }  
}

<font style="color:#000000;">myAnimal</font>引用仍然指向原来<font style="color:#000000;">myDog</font>所指向的<font style="color:#000000;">Dog</font>对象,但由于它被视为<font style="color:#000000;">Animal</font>类型,所以我们只能调用<font style="color:#000000;">Animal</font>类中定义的方法(和它的父类继承的方法),而不能调用<font style="color:#000000;">Dog</font>类特有的方法(如<font style="color:#000000;">bark()</font>)。

6.8.1 final 数据

用于指示一个变量、方法或类是不可变的。

1. **<font style="color:#000000;">final</font>** 变量

编译期常数(基本数据类型)

对于基本数据类型(如<font style="color:#000000;">int</font><font style="color:#000000;">double</font><font style="color:#000000;">char</font>等),<font style="color:#000000;">final</font>修饰的变量必须在声明时初始化,并且之后不能被重新赋值。这样的变量在编译时就可以确定其值,因此编译器可以对其进行优化。

public class FinalExample {  
    public static final int MAX_VALUE = 100; // 编译期常数  
    public static void main(String[] args) {  
        // MAX_VALUE = 200; // 这会编译错误,因为MAX_VALUE是final的  
        System.out.println(MAX_VALUE);  
    }  
}

成员变量(对象引用)

对于对象引用,<font style="color:#000000;">final</font>修饰的变量必须被初始化到一个对象,并且之后不能指向另一个对象,但对象本身的状态(即对象的字段)是可以被修改的。

public class Person {  
    private String name;  
  
    public Person(String name) {  
        this.name = name;  
    }  
  
    public void setName(String name) {  
        this.name = name; // 对象本身的状态可以改变  
    }  
}  
  
public class FinalReference {  
    public final Person person = new Person("Alice");  
  
    public void changePersonName() {  
        person.setName("Bob"); // 合法,因为只是修改了对象的状态  
        // person = new Person("Charlie"); // 这会编译错误,因为person是final的  
    }  
}

2. 空白**<font style="color:#000000;">final</font>**

声明为<font style="color:#000000;">final</font>但没有在声明时初始化的变量。这样的变量必须在构造器或初始化块中被初始化。

public class BlankFinal {  
    public final int number; // 空白final  
  
    public BlankFinal(int number) {  
        this.number = number; // 在构造器中初始化  
    }  
  
    public static void main(String[] args) {  
        BlankFinal bf = new BlankFinal(42);  
        System.out.println(bf.number);  
    }  
}

3. **<font style="color:#000000;">final</font>** 方法参数

在Java 1.1及以后版本中,方法参数也可以被声明为<font style="color:#000000;">final</font>。这表示在方法体内,你不能重新给参数赋值(即不能改变参数引用所指向的对象,但如果是对象参数,则可以修改其内部状态)。

public class FinalArgument {  
    public void printArray(final int[] numbers) {  
        // numbers = new int[5]; // 这会编译错误,因为numbers是final的  
        for (int i = 0; i < numbers.length; i++) {  
            numbers[i] = i * 2; // 合法,因为只是修改了数组的内容  
        }  
    }  
  
    public static void main(String[] args) {  
        int[] myArray = {1, 2, 3, 4, 5};  
        FinalArgument fa = new FinalArgument();  
        fa.printArray(myArray);  
        for (int num : myArray) {  
            System.out.println(num); // 输出修改后的数组  
        }  
    }  
}

6.8.2 final 方法

1. ** 防止覆盖** :当希望一个方法的行为在继承体系中保持不变,防止子类覆盖这个方法时,可以将这个方法声明为` final` 。 2. ** 性能优化** :虽然现代JVM的优化已经非常成熟,但在某些情况下,将方法声明为` final` 可以帮助编译器进行更有效的内联(inlining)优化,减少方法调用的开销。然而,这主要适用于非常小的方法。

6.8.3 final 类

+ ** 防止继承** :当你确信一个类不应该被继承时,可以将这个类声明为` final` 。这通常是因为这个类的设计是完整的,不需要通过继承来扩展或修改其功能。 + ** 性能考虑** :虽然` final` 类本身并不直接带来性能提升,但由于它禁止了继承,编译器可能更容易对类中的方法进行优化。

第7 章 多形性(多态)

```java class Note { private int value;
private Note(int val) {  
    value = val;  
}  
//定义三种音符
public static final Note middleC = new Note(0);  
public static final Note cSharp = new Note(1);  
public static final Note cFlat = new Note(2);  
// 可以继续添加其他音符  

}

class Instrument { //乐器大类,接受不同音调
public void play(Note n) {
System.out.println(“Instrument.play()”);
}
}

class Wind extends Instrument { //风琴
@Override
public void play(Note n) { //风琴演奏
System.out.println(“Wind.play()”);
}
}

public class Music {
public static void tune(Instrument i) { //发出声音,接收一个乐器
i.play(Note.middleC); //乐器调用play方法,发出一个音调
}

public static void main(String[] args) {  
    Wind flute = new Wind();  
    tune(flute); // Upcasting  
}  

}


**<font style="color:#000000;">动态绑定</font>**<font style="color:#000000;">发生在运行时,Java通过方法的实际运行时的对象类型来决定具体调用哪个方法。这通常用于多态的实现,是Java面向对象编程的重要特性之一。</font>

<font style="color:#000000;">当调用非final、非private、非static的方法时,如果这些方法在子类中可能被重写,那么Java就会在运行时确定到底调用哪个方法。</font>

<h2 id="q4fNU"><font style="color:#000000;">7.3 覆盖与过载</font></h2>
<font style="color:#000000;">方法重载(Overloading):在同一个类中,允许存在多个同名方法,只要它们的参数列表不同即可。这里的“不同”包括参数的类型、个数或顺序。重载是编译时多态的一种体现。</font>

<font style="color:#000000;">方法覆盖(Overriding):在子类中,可以定义一个与父类中具有相同名称、参数列表和返回类型(或协变返回类型)的方法,以覆盖父类中的方法。覆盖是运行时多态的一种体现。</font>

<h2 id="wDehl"><font style="color:#000000;">7.4 抽象类和方法</font></h2>
**<font style="color:#000000;">抽象类的用途和好处</font>**

<font style="color:#000000;">定义接口:抽象类可以定义一组必须由子类实现的接口,从而确保子类具有某些特定的功能。</font>

<font style="color:#000000;">提高代码复用性:通过继承抽象类,子类可以重用父类中定义的非抽象方法。</font>

<font style="color:#000000;">防止实例化:通过将类声明为抽象,可以防止其被实例化,这有助于确保类只被用作其他类的基类。</font>

<h2 id="T1AeH"><font style="color:#000000;">7.5 接口</font></h2>
| <font style="color:#000000;"></font> | **<font style="color:#000000;">接口(Interface)</font>** | **<font style="color:#000000;">抽象类(Abstract Class)</font>** |
| --- | --- | --- |
| **<font style="color:#000000;">定义</font>** | <font style="color:#000000;">一种完全抽象的引用类型,用于指定一组方法规范。</font> | <font style="color:#000000;">一种不能被实例化的类,用于定义一组子类的共同行为。</font> |
| **<font style="color:#000000;">实现/继承</font>** | <font style="color:#000000;">使用</font>`<font style="color:#000000;">implements</font>`<font style="color:#000000;">关键字实现接口。</font> | <font style="color:#000000;">使用</font>`<font style="color:#000000;">extends</font>`<font style="color:#000000;">关键字继承抽象类。</font> |
| **<font style="color:#000000;">多继承</font>** | <font style="color:#000000;">一个类可以实现多个接口。</font> | <font style="color:#000000;">一个类只能继承一个抽象类(Java不支持多重继承)。</font> |
| **<font style="color:#000000;">方法</font>** | <font style="color:#000000;">所有方法都是抽象的(Java 8前),或包含默认方法和静态方法(Java 8+)。</font> | <font style="color:#000000;">可以包含抽象方法和具体方法。</font> |
| **<font style="color:#000000;">字段</font>** | <font style="color:#000000;">字段自动是</font>`<font style="color:#000000;">public static final</font>`<font style="color:#000000;">的。</font> | <font style="color:#000000;">字段可以是任何访问级别,且可以是静态的或非静态的。</font> |
| **<font style="color:#000000;">构造函数</font>** | <font style="color:#000000;">接口不能有构造函数。</font> | <font style="color:#000000;">抽象类可以有构造函数。</font> |
| **<font style="color:#000000;">访问修饰符</font>** | <font style="color:#000000;">默认是</font>`<font style="color:#000000;">public</font>`<font style="color:#000000;">的,但也可以是包级私有的(即没有访问修饰符)。</font> | <font style="color:#000000;">可以是</font>`<font style="color:#000000;">public</font>`<font style="color:#000000;">、</font>`<font style="color:#000000;">protected</font>`<br/><font style="color:#000000;">、默认(包级私有)或</font>`<font style="color:#000000;">private</font>`<br/><font style="color:#000000;">的。</font> |
| **<font style="color:#000000;">默认实现</font>** | <font style="color:#000000;">不提供默认实现(除了默认方法和静态方法)。</font> | <font style="color:#000000;">可以提供部分具体方法的实现。</font> |
| **<font style="color:#000000;">用途</font>** | <font style="color:#000000;">主要用于定义对象的行为规范,实现接口的类必须遵循这些规范。</font> | <font style="color:#000000;">主要用于定义一组子类的共同行为,并提供部分实现。</font> |
| **<font style="color:#000000;">实例化</font>** | <font style="color:#000000;">接口不能被实例化。</font> | <font style="color:#000000;">抽象类不能被直接实例化,但可以通过其子类来实例化。</font> |


```java
// Java 8及更高版本的接口示例,包含默认方法和静态方法  
public interface NewStyleInterface {  
    // 这是一个抽象方法  
    void show();  
  
    // 这是一个默认方法,有方法体  
    default void defaultMethod() {  
        System.out.println("Default method implementation.");  
    }  
  
    // 这是一个静态方法  
    static void staticMethod() {  
        System.out.println("Static method implementation.");  
    }  
}  
  
// 实现接口的类必须提供抽象方法的具体实现,但可以选择性地覆盖默认方法  
public class NewStyleInterfaceImpl implements NewStyleInterface {  
    @Override  
    public void show() {  
        System.out.println("Show method implementation.");  
    }  
  
    // 可以选择性地覆盖默认方法,这里我们没有覆盖它  
    // 但如果我们想改变默认行为,我们可以这样做  
    // @Override  
    // public void defaultMethod() {  
    //     System.out.println("Overridden default method implementation.");  
    // }  
}  
  
// 演示如何使用接口中的静态方法  
public class InterfaceDemo {  
    public static void main(String[] args) {  
        // 静态方法可以通过接口名直接调用  
        NewStyleInterface.staticMethod();  
  
        // 创建实现接口的类的实例,并调用其方法  
        NewStyleInterfaceImpl impl = new NewStyleInterfaceImpl();  
        impl.show();  
        impl.defaultMethod(); // 调用默认方法,如果未被覆盖  
    }  
}

7.5.1/2 Java 的 继承

Java中的多重继承
在Java中,虽然类不支持多重继承,但接口支持多重继承。这意味着一个接口可以继承一个或多个其他接口,一个类也可以实现一个或多个接口。
interface CanFight {  
    void fight();  
}  
  
interface CanSwim {  
    void swim();  
}  
  
interface CanFly {  
    void fly();  
}  
  
class ActionCharacter {  
    void fight() {  
        System.out.println("Fighting!");  
    }  
}  
  
class Hero extends ActionCharacter implements CanFight, CanSwim, CanFly {  
    @Override  
    public void swim() {  
        System.out.println("Swimming!");  
    }  
  
    @Override  
    public void fly() {  
        System.out.println("Flying!");  
    }  
    // fight() 已经在 ActionCharacter 中定义,因此这里不需要重新实现  
}
通过继承扩展接口
Java可以创建一个新的接口,继承了一个或多个现有接口的所有方法签名。这通过` extends` 关键字实现.

例如:

interface Monster {  
    void roar();  
}  
  
interface DangerousMonster extends Monster {  
    void attack();  
}  
  
class Dragonzilla implements DangerousMonster {  
    @Override  
    public void roar() {  
        System.out.println("Roaring loudly!");  
    }  
  
    @Override  
    public void attack() {  
        System.out.println("Attacking fiercely!");  
    }  
}

7.5.3 常数分组

由于置入一个接口的所有字段都自动具有 static和 final属性,所以接口是对常数值进行分组的一个好工具,它具有与C或C++的enum 非常相似的效果。
// Months.java  
package c07;  
public interface Months {  
    int JANUARY = 1, FEBRUARY = 2, MARCH = 3, APRIL = 4,  
        MAY = 5, JUNE = 6, JULY = 7, AUGUST = 8,  
        SEPTEMBER = 9, OCTOBER = 10, NOVEMBER = 11, DECEMBER = 12;  
}

接口中的字段

默认是 public static final:在Java中,接口中定义的字段自动具有 public static final 的属性,这意味着这些字段是公开的、静态的、且最终的(即常量)。

不能是“空白final”:由于这些字段必须是final的,因此它们必须在声明时就被初始化,不能是所谓的“空白final”(即只声明了final但没有初始化的字段),因为这样的字段必须在构造函数中初始化,但接口没有构造函数。

7.6 内部类

1.成员内部类

成员内部类是最普通的内部类,它的定义为位于另一个类的内部比如:
class Circle {
    double radius = 0;
    public Circle(double radius) {
        this.radius = radius;}
    
    class Draw {     //内部类
        public void drawSahpe() {
            System.out.println("drawshape");
        }}}

成员内部类可以使用的访问修饰符包括:

public:公有的,表示该内部类可以被任意类访问。

protected:受保护的,表示该内部类可以被同包类访问,如果不是同包类,必须是该外部类的子类才可以访问。

默认(无修饰符):也称为包访问权限,表示该内部类只能被同包中的类访问。

private:私有的,表示该内部类只能在其外部类内部被访问,同包中的其他类也不能访问。

内部类访问外部类

  • 成员内部类可以无条件访问外部类的所有成员属性和成员方法(包括private成员和静态成员)。
  • 当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:

外部类.this.成员变量

外部类.this.成员方法

外部类访问内部类

  • 外部类中如果要访问内部类的成员,须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:

:::color2
getDrawInstance().drawSahpe(); //必须先创建成员内部类的对象,再进行访问

:::

创建成员内部类的对象,前提是必须存在一个外部类的对象。创建成员内部类对象的一般方式如下:

public class Test {
    public static void main(String[] args)  {
        //第一种方式:
        Outter outter = new Outter();
        Outter.Inner inner = outter.new Inner();  //必须通过Outter对象来创建
        //第二种方式:
        Outter.Inner inner1 = outter.getInnerInstance();}}

2.局部内部类

局部内部类是定义在一个 方法 或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。

局部内部类就像是方法里面的一个局部变量一样,是不能有 public、protected、private 以及 static 修饰符的。

class People{
    public People() {  }
}
 
class Man{
    public Man(){ }
     
    public People getWoman(){
        class Woman extends People{   //局部内部类
            int age =0;
        }
        return new Woman();
    }
}

3.匿名内部类

通常用于实现接口或继承其他类(通常是抽象类)时

特点

  1. 没有类名:这是匿名内部类最显著的特点,它直接通过<font style="color:#000000;">new</font>关键字后跟接口或类的类型以及大括号<font style="color:#000000;">{}</font>中的类体来定义。
  2. 必须实现接口或继承类:匿名内部类必须实现一个接口或继承一个类(通常是抽象类或具体的类,但继承具体类时,该类不能有显式的构造函数,或者需要调用无参构造函数)。
  3. 只能使用一次:由于匿名内部类没有名称,因此它不能被复用。每次需要使用时,都必须重新定义。
  4. 可以访问外部类的成员:匿名内部类可以访问其外部类的成员,包括私有成员(只要是在其定义的作用域内)。

使用场景

  • 当需要实现一个接口,但只需要一个该接口的实例时。
  • 当需要继承一个类,但只需要一个该类的实例,并且不需要为这个类添加额外的成员时。
interface Greeting {  
    void sayHello();  
}  
  
public class Test {  
    public static void main(String[] args) {  
        
        Greeting greeting = new Greeting() {  
            @Override  
            public void sayHello() {  
                System.out.println("Hello, World!");  
            }  
        };  
        greeting.sayHello();  
    }  
}
abstract class Animal {  
    abstract void makeSound();  
}  
  
public class Test {  
    public static void main(String[] args) {  
        Animal dog = new Animal() {  
            @Override  
            public void makeSound() {  
                System.out.println("Woof!");  
            }  
        };  
        dog.makeSound();  
    }  
}

4.静态内部类

静态内部类定义在另一个类里面的类,在类的前面多了一个关键字static。

静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法。

可以不创建外部类的实例而直接创建静态内部类的实例。静态内部类与普通的类(非内部类)非常相似,只是它被嵌套在另一个类的内部。

public class OuterClass {  
    // 外部类成员  
    private int outerValue = 100;  
  
    // 静态内部类  
    public static class StaticInnerClass {  
        // 静态内部类成员  
        private int innerValue = 200;  
  
        // 静态内部类的方法  
        public void display() {  // 静态内部类不能直接访问外部类的非静态成员  
            // 访问自己的成员  
            System.out.println("Inner Value: " + innerValue);  
        }  
    }  
  
    public static void main(String[] args) {  
        // 不需要外部类实例即可创建静态内部类的实例  
        OuterClass.StaticInnerClass inner = new OuterClass.StaticInnerClass();  
        inner.display();  
    }  
}

7.6.9 为什么要用内部类:控制框架

内部类不仅提供了一种封装和隐藏实现细节的方式,还允许内部类直接访问其外部类的成员。

控制框架与内部类的关系

控制框架是一种特殊的应用程序框架,它主要用于处理事件驱动的系统。在Java中,AWT(Abstract Window Toolkit)和Swing等GUI工具包就是典型的控制框架。这些框架通过事件监听和响应机制来处理用户的输入和系统的状态变化。

内部类在控制框架中的作用主要体现在以下几个方面:

封装实现细节:内部类可以将与特定事件处理相关的代码封装在外部类中,从而保持代码的整洁和模块化。这样,外部类就充当了一个容器,而内部类则负责具体的实现细节。

直接访问外部类成员:内部类可以直接访问其外部类的成员(包括私有成员),这使得在事件处理中访问和修改外部类的状态变得非常方便。

简化代码结构:通过将事件处理代码作为内部类实现,可以避免在外部类中创建大量的匿名内部类或实现多个接口,从而使代码结构更加清晰。

内部类的优势

封装性:内部类可以将实现细节隐藏在其外部类中,减少了对外部世界的暴露。

直接访问性:内部类可以直接访问其外部类的成员,包括私有成员,这简化了代码并提高了效率。

灵活性:内部类可以作为匿名类使用,或者实现多个接口,提供了更大的灵活性。

代码组织:内部类有助于将相关的代码组织在一起,提高了代码的可读性和可维护性。

7.7 构建器和多形性

7.7.1 构建器的调用顺序

** 构建器调用顺序**
  1. 静态初始化块

静态成员和静态初始化块:在类的任何实例被创建之前,静态成员(包括静态变量和静态初始化块)会被初始化。静态初始化块在类加载到JVM时执行,且只执行一次。静态成员属于类本身,而不是类的某个特定实例。

  1. 调用基础类(父类)的构建器

从Object类开始:如果你没有显式地指定其他基类,那么最顶层的基类就是Object类。首先会调用Object类的无参构建器。

向上移动:然后,按照继承层次结构,逐级向上调用每个父类的构建器。这个过程会一直进行,直到达到当前类的直接父类。

  1. 成员初始化

成员变量初始化:在调用当前类的构建器主体之前,会按照成员变量声明的顺序初始化它们。如果成员变量是对象,则会先调用这些对象的构建器。

实例初始化块:实例初始化块是在创建对象时自动执行的代码块,它们在构建器之前执行,并且每次创建对象时都会执行。如果有多个实例初始化块,它们将按照在类中出现的顺序执行。

  1. 调用当前类的构建器主体

构建器代码:最后,执行当前类构建器的代码块。在这个阶段,所有的成员变量(包括对象成员)都已经被初始化,因此你可以在构建器主体中安全地使用它们。

7.7.3 构建器内部的多形性方法的行为

在Glyph类的构造器中调用了draw()方法,这是一个抽象方法,在RoundGlyph类中被具体实现。然而,当Glyph的构造器执行时,虽然它调用了draw(),但此时RoundGlyph类的实例尚未完全构造完成。特别是,RoundGlyph类的成员变量radius还没有被赋予用户提供的值,而是仍然保持着Java为其分配的默认值0。

解决方案

避免在构造器中调用动态绑定方法:尽量在构造器中只进行必要的初始化工作,避免调用任何可能依赖于尚未完全初始化状态的方法。

使用final或private方法:如果确实需要在构造器中调用方法,确保这些方法在基础类中声明为final或private,这样它们就不会被覆盖,从而避免了上述问题。

7.8 通过继承进行设计

** 继承 vs 合成**

继承:

继承允许我们定义一个类的特殊化版本,继承类(子类)会继承基类(父类)的属性和方法,并且可以添加或覆盖它们。

继承表达的是“是一个”(is-a)的关系。例如,猫是动物的一个特殊种类。

继承有助于代码复用,但过度使用会导致类层次结构过于复杂,难以理解和维护(即所谓的“继承税”)。

多个类共享某些行为但有细微差别时,可以使用继承来定义这些共享行为,并在子类中覆盖或添加特定行为。

合成:

合成是通过在类中包含另一个类的对象作为成员变量来实现的。

合成表达的是“有一个”(has-a)的关系。例如,一个舞台有一个演员。

合成提供了更高的灵活性,因为可以在运行时动态地改变对象的状态或行为。

当需要在运行时改变对象的状态(包括其行为)时,合成是一个更好的选择。通过包含其他类型的对象作为成员变量,并在运行时修改这些对象的引用,可以实现灵活的状态管理。

7.8.2 下潮造型与运行期类型标识

运行时类型标识(RTTI, Run-Time Type Identification)是一个强大的特性,它允许我们在运行时查询和操作对象的实际类型。这是通过instanceof关键字和类型转换(casting)来实现的。

上溯造型和下溯造型

上溯造型:将一个派生类(子类)的对象引用赋给一个基类(父类)的引用。这是安全的,因为派生类对象总是包含基类对象的所有信息。在Java中,这种转换是隐式的,不需要特殊操作。

下溯造型:将一个基类(父类)的引用转换成一个派生类(子类)的引用。这是不安全的,因为基类引用可能实际上并不指向一个派生类对象。因此,这种转换需要显式地进行,并且在运行时由JVM进行检查以确保类型安全。

使用instanceof关键字

在进行下溯造型之前,通常使用instanceof关键字来检查一个对象是否是特定类的实例。这可以避免ClassCastException的发生。

if (x[1] instanceof MoreUseful) { //检查数组x中的第二个元素(索引为1)
                                  //是否是MoreUseful类或其子类的实例。
    ((MoreUseful)x[1]).u(); // 将x[1]强制转换(cast)为MoreUseful类型,
                            //调用MoreUseful对象的u()方法
}

第8章 对象的容纳

8.1 数组

8.1.1 数组和第一类对象

** 数组(Arrays)**
// 基本数据类型数组  
int[] e; // 声明一个整型数组变量e,但未初始化  
int[] f = new int[5]; // 声明并初始化一个整型数组f,包含5个元素,默认初始化为0  
int[] g = {1, 2, 3, 4, 5}; // 声明并初始化一个整型数组g,包含5个指定元素  
  
// 对象数组  
Weeble[] a; // 声明一个Weeble类型的数组变量a,但未初始化  
Weeble[] b = new Weeble[5]; // 声明并初始化一个Weeble类型的数组b,包含5个元素,但每个元素初始化为null  
Weeble[] c = new Weeble[4];  
for(int i = 0; i < c.length; i++) {  
    c[i] = new Weeble(); // 显式地为每个元素分配一个Weeble对象  
}  
Weeble[] d = {new Weeble(), new Weeble(), new Weeble()}; // 使用集合初始化语法

8.1.2 数组的返回

在Java中,当从方法返回一个数组时,实际上返回的是对该数组对象的“指针”。这个引用指向了堆内存中的一个数组对象,而不是数组本身的内容的拷贝。因此,java可以像传递其他对象一样传递和返回数组。

8.2 集合

Java 提供了四种类型的“集合类”:Vector(矢量)、BitSet(位集)、Stack(堆栈)以及 Hashtable(散列表)。

8.2.1 缺点:类型未知

集合允许开发者将任何类型的对象放入集合中,而不需要在编译时进行类型检查。Java所有的对象类型都继承自Object类,所以在将对象放入集合时,类型信息可能会丢失。这可能导致一些类型安全问题,比如将错误类型的对象放入了期望特定类型对象的集合中。

泛型

通过泛型,可以在编译时指定集合将要容纳的对象类型,这样编译器就可以进行类型检查,防止错误类型的对象被添加到集合中。

Vector<Cat> cats = new Vector<>();  
cats.add(new Cat(1)); // 正确  
// cats.add(new Dog(1)); // 编译错误,因为Vector<Cat>只能添加Cat类型的对象       

错误有时并不显露出来
将一个对象与字符串进行连接时,Java编译器会自动调用该对象的 <font style="color:#000000;">toString()</font> 方法来获取其字符串表示。

8.3 枚举器(反复器)

` Enumeration` 接口是一个早期的集合遍历机制,它提供了一种方式来遍历集合中的元素。

<font style="color:#000000;">Enumeration</font> 主要包含两个方法:<font style="color:#000000;">hasMoreElements()</font><font style="color:#000000;">nextElement()</font>

<font style="color:#000000;">hasMoreElements()</font> 方法用于检查集合中是否还有更多的元素可以遍历

<font style="color:#000000;">nextElement()</font> 方法则用于获取集合中的下一个元素。

static void printAll(Enumeration e) {
 while(e.hasMoreElements())
 System.out.println(
 e.nextElement().toString());
}

8.4 集合的类型

1. Vector

` Vector` 是Java中的一个动态数组,它的大小可以根据需要增加或减少。` Vector` 是同步的,这意味着它在多线程环境中是线程安全的,但这也导致了它的性能相对较低。
        Vector<String> vector = new Vector<>();  //声明&添加
        vector.add("Apple");  
        vector.add("Banana");  
        vector.add("Cherry");  
  
        System.out.println(vector); // 输出:[Apple, Banana, Cherry]  
  
        // 访问元素  
        System.out.println(vector.get(1)); // 输出:Banana  
  
        // 移除元素  
        vector.remove(0);  
        System.out.println(vector); // 输出:[Banana, Cherry]  

2. BitSet

` BitSet` 是一个用于处理位集合的类。它可以存储和管理一个位序列,并提供了一系列的方法来设置、清除和检查各个位。
        BitSet bits = new BitSet();  
  
        // 设置位  
        bits.set(0);  
        bits.set(2);  
  
        // 打印BitSet
        for (int i = 0; i < 5; i++) {  
            System.out.print(bits.get(i) ? "1" : "0");  
        }  
        System.out.println(); // 输出:10100  
  
        // 清除位  
        bits.clear(2);  
  
        // 再次打印BitSet  
        for (int i = 0; i < 5; i++) {  
            System.out.print(bits.get(i) ? "1" : "0");  
        }  
        System.out.println(); // 输出:10000  

3. Stack

` Stack` 是一个后进先出(LIFO)的栈实现,它继承自` Vector` 。` Stack` 提供了额外的方法来表示栈操作(如` push` 和` pop` ),它继承自` Vector` ,具有` Vector` 的所有方法。
        Stack<String> stack = new Stack<>();  
  
        // 压栈  
        stack.push("Apple");  
        stack.push("Banana");  
  
        // 查看栈顶元素  
        System.out.println(stack.peek()); // 输出:Banana  
  
        // 弹栈  
        System.out.println(stack.pop()); // 输出:Banana  
  
        // 再次查看栈顶元素  
        System.out.println(stack.peek()); // 输出:Apple  

4.Hashtable

```java import java.util.HashMap; // 引入 HashMap 类

HashMap<Integer, String> Sites = new HashMap<Integer, String>();

// 添加键值对
Sites.put(1, “Google”);
Sites.put(2, “Runoob”);
Sites.put(3, “Taobao”);
Sites.put(4, “Zhihu”);

//访问元素
System.out.println(Sites.get(3));

//删除元素
Sites.remove(4);

//删除所有键值对(key-value)可以使用 clear 方法:
Sites.clear();

//如果要计算 HashMap 中的元素数量可以使用 size() 方法:
System.out.println(Sites.size());

// 输出 key 和 value,获取 key,可以使用 keySet() 方法,
//获取 value,可以使用 values() 方法。
for (Integer i : Sites.keySet()) {
System.out.println("key: " + i + " value: " + Sites.get(i));
}
// 返回所有 value 值
for(String value: Sites.values()) {
// 输出每一个value
System.out.print(value + ", ");
}
//替换元素
// key - 键
// oldValue - 旧的 value 值
// newValue - 新的 value 值
hashmap.replace(K key, V newValue)
hashmap.replace(K key, V oldValue, V newValue)


<h2 id="p7l0d"><font style="color:#000000;"> 8.7 新集合  </font></h2>
<h3 id="wzVnL"><font style="color:#000000;"> 8.7.1 使用 Collections  </font></h3>
<font style="color:#000000;">Collections 是 JDK 提供的一个工具类,位于 java.util 包下,提供了一系列的静态方法,方便我们对集合进行各种操作,算是集合框架的一个大管家。</font>

**<font style="color:#000000;">排序操作</font>**

+ `<font style="color:#000000;">reverse(List list)</font>`<font style="color:#000000;">:反转顺序</font>
+ `<font style="color:#000000;">shuffle(List list)</font>`<font style="color:#000000;">:洗牌,将顺序打乱</font>
+ `<font style="color:#000000;">sort(List list)</font>`<font style="color:#000000;">:自然升序</font>
+ `<font style="color:#000000;">sort(List list, Comparator c)</font>`<font style="color:#000000;">:按照自定义的比较器排序</font>
+ `<font style="color:#000000;">swap(List list, int i, int j)</font>`<font style="color:#000000;">:将 i 和 j 位置的元素交换位置</font>

**<font style="color:#000000;">查找操作</font>**

+ `<font style="color:#000000;">binarySearch(List list, Object key)</font>`<font style="color:#000000;">:二分查找法,前提是 List 已经排序过了</font>
+ `<font style="color:#000000;">max(Collection coll)</font>`<font style="color:#000000;">:返回最大元素</font>
+ `<font style="color:#000000;">max(Collection coll, Comparator comp)</font>`<font style="color:#000000;">:根据自定义比较器,返回最大元素</font>
+ `<font style="color:#000000;">min(Collection coll)</font>`<font style="color:#000000;">:返回最小元素</font>
+ `<font style="color:#000000;">min(Collection coll, Comparator comp)</font>`<font style="color:#000000;">:根据自定义比较器,返回最小元素</font>
+ `<font style="color:#000000;">fill(List list, Object obj)</font>`<font style="color:#000000;">:使用指定对象填充</font>
+ `<font style="color:#000000;">frequency(Collection c, Object o)</font>`<font style="color:#000000;">:返回指定对象出现的次数</font>

**<font style="color:#000000;">替换操作</font>**

+ <font style="color:#000000;">replaceAll</font><font style="color:#000000;">(names,</font><font style="color:#000000;">"John"</font><font style="color:#000000;">,</font><font style="color:#000000;">"Jonathan"</font><font style="color:#000000;">);</font>

<h3 id="GHcOm"><font style="color:#000000;"> 8.7.2 使用 Lists  </font></h3>
<h4 id="raGyB">**<font style="color:#000000;">ArrayList</font>**</h4>
<font style="color:#000000;">ArrayList 类是一个可以动态修改的数组,与普通数组的区别就是它是没有固定大小的限制,我们可以添加或删除元素。</font>

```java
import java.util.ArrayList; // 引入 ArrayList 类

ArrayList<E> objectName =new ArrayList<>();  // 初始化

//添加元素到 ArrayList 可以使用 add() 方法:
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Weibo");

//访问 ArrayList 中的元素可以使用 get() 方法:
System.out.println(sites.get(1));  // 访问第二个元素

// 如果要修改 ArrayList 中的元素可以使用 set() 方法, 
// set(int index, E element) 
// 第一个参数是索引(index),表示要替换的元素的位置,
// 第二个参数是新元素(element),表示要设置的新值:
 sites.set(2, "Wiki"); // 第一个参数为索引位置,第二个为要修改的值

// 删除 ArrayList 中的元素可以使用 remove() 方法:
 sites.remove(3); // 删除第四个元素

// 计算 ArrayList 中的元素数量可以使用 size() 方法:
System.out.println(sites.size());

// 使用 for 来迭代数组列表中的元素:
for (int i = 0; i < sites.size(); i++) {
    System.out.println(sites.get(i));
}

// 使用 for-each 来迭代元素:
 for (String i : sites) {
    System.out.println(i);
}
**LinkedList**
链表(Linked list)是一种常见的基础数据结构,是一种线性表,但是并不会按线性的顺序存储数据,而是在每一个节点里存到下一个节点的地址。
// 引入 LinkedList 类
import java.util.LinkedList; 

LinkedList<E> list = new LinkedList<E>();   // 创建方法

// 创建
LinkedList<String> sites = new LinkedList<String>();
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Weibo");

// 使用 addFirst() 在头部添加元素
sites.addFirst("Wiki");

// 使用 addLast() 在尾部添加元素
sites.addLast("Wiki");

// 使用 removeFirst() 移除头部元素
sites.removeFirst();

// 使用 removeLast() 移除尾部元素
sites.removeLast();

// 使用 getFirst() 获取头部元素
System.out.println(sites.getFirst());

// 使用 getLast() 获取尾部元素
System.out.println(sites.getLast());

// for 配合 size() 方法来迭代列表中的元素:
for (int size = sites.size(), i = 0; i < size; i++) {
    System.out.println(sites.get(i));
}

// 使用 for-each 来迭代元素:
for (String i : sites) {
    System.out.println(i);
}

8.7.3 使用 Sets

**HashSet**
HashSet 是一个不允许有重复元素的集合。HashSet 允许有 null 值。

HashSet 是无序的,即不会记录插入的顺序。

import java.util.HashSet; // 引入 HashSet 类

HashSet<String> sites = new HashSet<String>();

// 添加元素可以使用 add() 方法:
sites.add("Google");
sites.add("Runoob");
sites.add("Taobao");
sites.add("Zhihu");
sites.add("Runoob");  // 重复的元素不会被添加

// 使用 contains() 方法来判断元素是否存在于集合当中:
System.out.println(sites.contains("Taobao"));//输出T||F

// 使用 remove() 方法来删除集合中的元素:
sites.remove("Taobao");  // 删除元素,删除成功返回 true,否则为 false

// 删除集合中所有元素可以使用 clear 方法:
sites.clear();  

// 计算 HashSet 中的元素数量可以使用 size() 方法:
System.out.println(sites.size());  

// 使用 for-each 来迭代 HashSet 中的元素。
for (String i : sites) {
    System.out.println(i);
}

8.7.4 使用 Maps

HashMap
HashMap 是一个散列表,它存储的内容是键值对(key-value)映射。

HashMap 是无序的,即不会记录插入的顺序。

import java.util.HashMap; // 引入 HashMap 类

HashMap<Integer, String> Sites = new HashMap<Integer, String>();

// 添加键值对
Sites.put(1, "Google");
Sites.put(2, "Runoob");
Sites.put(3, "Taobao");
Sites.put(4, "Zhihu");

// get(key) 方法来获取 key 对应的 value:
System.out.println(Sites.get(3));

// 使用 remove(key) 方法来删除 key 对应的键值对(key-value):
Sites.remove(4);

// 删除所有键值对(key-value)可以使用 clear 方法:
Sites.clear();

// 计算 HashMap 中的元素数量可以使用 size() 方法:
System.out.println(Sites.size());

// for-each 来迭代 HashMap 中的元素。
for (Integer i : Sites.keySet()) {
    System.out.println("key: " + i + " value: " + Sites.get(i));
}
**TreeMap**
```java // TreeMap

// put() - 将指定的键/值映射(条目)插入到映射中
evenNumbers.put(“Two”, 2);
evenNumbers.put(“Four”, 4);

// putIfAbsent() - 如果映射中不存在指定的键值,则将指定的键/值映射插入到map中
evenNumbers.putIfAbsent(“Six”, 6);

// get() - 返回与指定键关联的值。如果找不到键,则返回null。
int value1 = numbers.get(“Three”);

// entrySet() - 返回TreeMap的所有键/值映射(条目)的集合
// keySet() - 返回TreeMap的所有键的集合
// values() - 返回TreeMap的所有值的集合
System.out.println("Key/Value 映射: " + numbers.entrySet());
System.out.println("Keys: " + numbers.keySet());
System.out.println("Values: " + numbers.values());

// remove(key) - 返回并从TreeMap中删除与指定键关联的条目
int value = numbers.remove(“Two”);
System.out.println("被删除的值: " + value);

// remove(key, value) -仅当指定键与指定值相关联时才从映射中删除条目,并返回布尔值
boolean result = numbers.remove(“Three”, 3);
System.out.println("条目 {Three=3} 被删除? " + result);

// replace(key, value)-用key新的替换指定映射的值value
numbers.replace(“Second”, 22);
// replace(key, old, new) -仅当旧值已与指定键关联时,才用新值替换旧值
numbers.replace(“Third”, 3, 33);

// firstKey() - 返回map的第一个键
// firstEntry() - 返回映射的第一个键的键/值映射
// lastKey() - 返回map的最后一个键
// lastEntry() - 返回映射的最后一个键的键/值映射


<font style="color:#000000;">"当需要存储键值对且不关心键的排序时,使用HashMap;当需要键自然排序或自定义排序的键值对时,使用TreeMap。"</font>

<h3 id="qdUJ7"><font style="color:#000000;"> 8.7.7 排序和搜索  </font></h3>
<h4 id="V5rhH"><font style="color:#000000;">数组</font></h4>
`<font style="color:#000000;">Arrays.sort()</font>`<font style="color:#000000;"> 对整数数组进行排序, </font>`<font style="color:#000000;">Arrays.binarySearch()</font>`<font style="color:#000000;"> 对排序后的数组进行搜索。</font>

```java
import java.util.Arrays;  
        // 初始化一个未排序的整数数组  
        int[] numbers = {5, 2, 9, 1, 5};  
  
        // 使用Arrays.sort()对数组进行排序  
        Arrays.sort(numbers);  
  
        // 使用Arrays.binarySearch()搜索数组中的元素  
        int searchValue = 5;  
        int index = Arrays.binarySearch(numbers, searchValue);  
        // 如果index < 0,表示未找到元素  
import java.util.Arrays;  

        // 初始化一个未排序的字符串数组  
        String[] fruits = {"banana", "apple", "orange", "mango", "grape"};  
  
        // 使用Arrays.sort()对数组进行排序  
        Arrays.sort(fruits);  

        // 使用Arrays.binarySearch()搜索数组中的元素  
        String searchValue = "mango";  
        int index = Arrays.binarySearch(fruits, searchValue);  
  
        // 如果index >= 0,表示找到了元素  
可比较与比较器
Comparable

要求实现<font style="color:#000000;">compareTo(T o)</font>方法,该方法接受一个同类型的对象作为参数,并返回一个整数来表示比较结果。如果当前对象小于、等于或大于参数对象,则分别返回负数、零或正数。

实现该接口的类将排序逻辑嵌入到类本身中,因此只能有一种排序方式(即自然排序)。

Comparator

要求实现<font style="color:#000000;">compare(T o1, T o2)</font>方法,方法接受两个同类型的对象作为参数,并返回一个整数来表示<font style="color:#000000;">o1</font><font style="color:#000000;">o2</font>的比较结果。如果<font style="color:#000000;">o1</font>小于、等于或大于<font style="color:#000000;">o2</font>,则分别返回负数、零或正数。

可以为同一个类定义多个Comparator实现,每个实现都可以提供不同的排序规则。这使得排序更加灵活。

public class Person implements Comparable<Person> {  
    private String name;  
    private int age;  
  
    // 构造函数、getter和setter省略  
  
    @Override  
    public int compareTo(Person other) {  
        return Integer.compare(this.age, other.age); // 按年龄排序  
    }  

    // 使用Collections.sort()进行排序
    Collections.sort(persons);  
}
public class Person {  
    private String name;  
    private int age;  
  
    // 构造函数、getter和setter省略  
  
    // 使用Comparator进行排序  
    public static void main(String[] args) {  
        List<Person> people = new ArrayList<>();  
        // 添加Person对象到列表  
  
        Collections.sort(people, new Comparator<Person>() {  
            @Override  
            public int compare(Person p1, Person p2) {  
                return p1.getName().compareTo(p2.getName()); // 按姓名排序  
            }  
        });  
    }  
}

第9 章 违例差错控制

9.1 基本违例

异常处理允许程序在出现错误时能够优雅地恢复,而不是直接崩溃。

检查性异常

检查性异常是编译器要求必须处理的异常。如果方法中可能会抛出检查性异常,必须在中使用throws,或者try-catch语句块来捕获并处理这个异常。比如:

IOException:在进行输入输出操作时可能发生的异常。

SQLException:在进行数据库操作时可能发生的异常。

ClassNotFoundException:在尝试加载类到JVM时,未找到指定的类时抛出的异常。

运行时异常

这类异常通常是由编程错误导致的,比如数组越界、空指针引用等。比如:

NullPointerException:尝试在需要对象的地方使用了null。

ArrayIndexOutOfBoundsException:数组索引越界。

Exception类

是所有异常类的父类。

继承自 Throwable 类。

Java 内置异常类:

异常描述
ArithmeticException当出现异常的运算条件时,抛出此异常。例如,一个整数"除以零"时,抛出此类的一个实例。
ArrayIndexOutOfBoundsException用非法索引访问数组时抛出的异常。如果索引为负或大于等于数组大小,则该索引为非法索引。
ArrayStoreException试图将错误类型的对象存储到一个对象数组时抛出的异常。
ClassCastException当试图将对象强制转换为不是实例的子类时,抛出该异常。
IllegalArgumentException抛出的异常表明向方法传递了一个不合法或不正确的参数。
NegativeArraySizeException如果应用程序试图创建大小为负的数组,则抛出该异常。
NullPointerException当应用程序试图在需要对象的地方使用 <font style="color:#000000;">null</font>时,抛出该异常
NumberFormatException当应用程序试图将字符串转换成一种数值类型,但该字符串不能转换为适当格式时,抛出该异常。
StringIndexOutOfBoundsException此异常由 <font style="color:#000000;">String</font>方法抛出,指示索引或者为负,或者超出大小。

9.2 捕获异常

```java try{ // 程序代码 }catch(ExceptionName e1){ //Catch 块 } ```
try{
   // 程序代码
}catch(异常类型1 异常的变量名1){   //可以在 try 语句后面添加任意数量的 catch 块。
  // 程序代码                     //如果保护代码中发生异常,异常被抛给第一个 catch 块。
}catch(异常类型2 异常的变量名2){   //如果匹配,就会被第一个捕获。
  // 程序代码                     //如果不匹配,它会被传递给第二个 catch 块。
}catch(异常类型3 异常的变量名3){   //如此,直到异常被捕获或者通过所有的 catch 块。
  // 程序代码
}
public void checkNumber(int num) {
  if (num < 0) {
    throw new IllegalArgumentException("Number must be positive");
  }
}
// throws用于在方法声明中指定该方法可能抛出的异常。
// 当方法内部抛出指定类型的异常时,会被传递给调用该方法的代码,并在该代码中处理异常。
public void readFile(String filePath) throws IOException {
  BufferedReader reader = new BufferedReader(new FileReader(filePath));
  String line = reader.readLine();
  while (line != null) {
    System.out.println(line);
    line = reader.readLine();
  }
  reader.close();
}

9.4 自定义异常类

```java // BookNotFoundException.java public class BookNotFoundException extends Exception { public BookNotFoundException(String message) { super(message); } 含了一个带有消息字符串的构造器,它将这个消息传递给Exception类的构造器。 } ```
// Library.java   
public class Library {  
    private Map<String, String> books = new HashMap<>();  
  
    public void addBook(String title, String author) {  
        books.put(title, author);  
    }  
  
    public String borrowBook(String title) throws BookNotFoundException {  
        if (!books.containsKey(title)) {  
            throw new BookNotFoundException("Book " + title + " not found.");  
        }  //如果请求的书不存在,将抛出一个BookNotFoundException。
        return books.remove(title);  
    }  
}
// Main.java  
public class Main {  
    public static void main(String[] args) {  
        Library library = new Library();  
        library.addBook("Java Concurrency in Practice", "Brian Goetz");  
  
        try {  
            String author = library.borrowBook("Java Concurrency in Practice");  
            System.out.println("Borrowed book by: " + author);  //正确输出
  
            // 尝试借出一本不存在的书  
            author = library.borrowBook("Effective Java");  
            System.out.println("Borrowed book by: " + author);  
        } catch (BookNotFoundException e) {  
            System.out.println(e.getMessage());  //继承于Exception
        }  
    }  
}

9.6 finally关键字

无论是否发生异常,finally 代码块中的代码总会被执行。

可以运行清理类型等收尾善后性质的语句。

try{
  // 程序代码
}catch(异常类型1 异常的变量名1){
  // 程序代码
}catch(异常类型2 异常的变量名2){
  // 程序代码
}finally{
  // 程序代码
}
public class ExcepTest{
  public static void main(String args[]){
    int a[] = new int[2];
    try{
       System.out.println("Access element three :" + a[3]);
    }catch(ArrayIndexOutOfBoundsException e){
       System.out.println("Exception thrown  :" + e);
    }
    finally{
       a[0] = 6;
       System.out.println("First element value: " +a[0]);
       System.out.println("The finally statement is executed");
    }
  }
}

finally中如果抛出异常,可能会覆盖try块中的异常.解决方法:

在finally块中捕获并处理异常:

如果finally块中的代码可能抛出异常,可以在finally块内部使用try-catch语句来捕获并处理这些异常。这样可以确保finally块中的异常不会干扰到try块中的异常处理。

try-with-resources

try-with-resources 是一种异常处理机制,它能够自动关闭在 try 块中声明的资源,无需显式地在 finally 块中关闭。
// 只需要在 try 关键字后面声明资源,然后跟随一个代码块。
// 无论代码块中的操作是否成功,资源都会在 try 代码块执行完毕后自动关闭。
try (resource declaration) {
  // 使用的资源
} catch (ExceptionType e1) {
  // 异常块
}
public static void main(String[] args) {
String line;
    // try-with-resources 语句中可以声明多个资源,方法是使用分号 ; 分隔各个资源:
    try(BufferedReader br = new BufferedReader(new FileReader("test.txt"))) {
        while ((line = br.readLine()) != null) {
            System.out.println("Line =>"+line);
        }
    } catch (IOException e) {
        System.out.println("IOException in try block =>" + e.getMessage());
    }
}
// 在 try-with-resources 语句中声明和实例化 BufferedReader 对象,
// 执行完毕后实例资源,不需要考虑 try 语句是正常执行还是抛出异常。
public static void main(String[] args) {
    BufferedReader br = null;
    String line;
    try {
        System.out.println("Entering try block");
        br = new BufferedReader(new FileReader("test.txt"));
        while ((line = br.readLine()) != null) {
        System.out.println("Line =>"+line);
        }
    } catch (IOException e) {
        System.out.println("IOException in try block =>" + e.getMessage());
    } finally {
        System.out.println("Entering finally block");
        try {
            if (br != null) {
                br.close();
            }
        } catch (IOException e) {
            System.out.println("IOException in finally block =>"+e.getMessage());
        }
    }

第10 章 Java IO 系统

:::color2 IO分为:输入流(读取),输出流(写出)

:::

:::color2
IO又分为:字节流(操作所有类型文件,包括音频,视频,图片,文本)

字符流(只能操作纯文本文件,word excel文件除外)

:::

字节流

字节输入流:InputStream(抽象类不能直接创建对象)

FilelnputStream
操作本地文件的字节输入流,可以把本地文件中的数据读取到程序中来。
  1. 创建字节输入流对象

如果文件不存在,就直接报错,

//1.创建对象
FileInputStream fis = new FileInputstream("a.txt");
  1. 读取数据

一次读一个字节,读出来的是数据在ASCII上对应的数字。

读到文件末尾,read方法返回-1。

public int read():一次读一个字节数据

public int read(byte[] buffer):一次读一个字节数组数据,适合读取大文件

 // 2. 读取数据  
 int data;  
 while ((data = fis.read()) != -1) { // 读取一个字节,如果到文件末尾则返回-1  
     // 将读取到的字节(在ASCII表中对应的数字)转换为字符  
     char c = (char) data;  
     System.out.print(c); // 输出字符  
  1. 释放资源

每次使用完流必须要释放资源。

**字节输出流:OutputStream(抽象类不能直接创建对象)**

FileOutputStream
操作本地文件的字节输出流,可以把程序中的数据写到本地文件中
  1. 创建字节输出流对象

1:参数是字符串表示的路径或者File对象都是可以的

2:如果文件不存在会创建一个新的文件,但是要保证父级路径是存在的,

3:如果文件已经存在,则会清空文件

// 使用文件路径字符串  
FileOutputStream fos1 = new FileOutputStream("example.txt",true/*续写时加上*/);  
// 或者使用File对象  
File file = new File("anotherExample.txt");  
FileOutputStream fos2 = new FileOutputStream(file);
  1. 写数据

write方法的参数是整数,但是实际上写到本地文件中的是整数在ASCII上对应的字符

void write(int b):一次写一个字节数据

void write(byte[] b):一次写一个字节数组数据

void write(byte[] b, int off, int len):一次写一个字节数组的部分数据

String data = "Hello, FileOutputStream!";  
byte[] dataBytes = data.getBytes(); // 将字符串转换为字节数组  
  
try (FileOutputStream fos = new FileOutputStream("output.txt")) {  
    fos.write(dataBytes); // 写入整个字节数组  
} catch (IOException e) {  
    e.printStackTrace();  
}

fos.write()
  1. 释放资源

每次使用完流之后都要释放资源

fos.close();

大文件拷贝

```java //1.创建对象 FileInputStream fis = new FileInputstream("movie.mp4"); FileOutputstream fos = new FileOutputStream("copy.mp4"); //2.拷贝 int len; byte[] bytes =new byte[1024*1024 * 5]; while((len =fis.read(bytes))!= -1){ fos.write(bytes,0,len); } //3.释放资源 fos.close(); fis.close(); ```

字符流

字符流=字节流+字符集,非常适合纯文本。

输入流:一次读一个字节,遇到中文时,一次读多个字节

输出流:底层会把数据按照指定的编码方式进行编码,变成字节再写到文件中

字符输入流:Reader**(抽象类不能直接创建对象)**

FileReader
public int read():读取数据,读到末尾返回-1

public int read(char[] buffer):读取多个数据,读到末尾返回-1

//1.创建对象并关联本地文件
FileReader fr = new FileReader("a.txt");

//2.1读取数据 无参read()
int ch;
while((ch=fr.read())!= -1){
    System.out.print((char)ch);
}
//2.2读取数据 有参read()
char[]chars = new char[2];
int len;
while((len =fr.read(chars))!= -1){
    //把数组中的数据变成字符串再进行打印
    System.out.print(new string(chars,0,len));
}

//3.释放资源
fr.close();

字符输出流:Writer**(抽象类不能直接创建对象)**

> public FileWriter(File file):创建字符输出流关联本地文件 > > public FileWriter(String pathname):创建字符输出流关联本地文件 > > public FileWriter(File file,boolean append):创建字符输出流关联本地文件,续写 > > public FileWriter(String pathname,boolean append):创建字符输出流关联本地文件,续写 > > >

void write(int c):写出一个字符

void write(string str):写出一个字符串

void write(string str,int off, int len):写出一个字符串的一部分

void write(char[] cbuf):写出一个字符数组

void write(char[]cbuf,int off, int len):写出字符数组的一部分

目录列表器

列出目录中的文件和子目录,使用` File` 类的` listFiles()` 方法。该方法返回一个` File[]` 数组,包含了目录中的所有文件和子目录。
import java.io.File;  
  
public class DirectoryLister {  
    public static void listFiles(String directoryPath) {  
        File directory = new File(directoryPath);  
  
        // 检查目录是否存在  
        if (!directory.exists()) {  return;  }  
  
        // 检查是否为目录  
        if (!directory.isDirectory()) {  return;  }  
  
        // 列出目录中的所有文件和子目录  
        File[] files = directory.listFiles();  
        if (files != null) {  
            for (File file : files) {  
                // 判断是文件还是目录  
                if (file.isFile()) {  
                    System.out.println("File: " + file.getName());  
                } else if (file.isDirectory()) {  
                    System.out.println("Directory: " + file.getName());  
                }  
            }  
        } else {  
            System.out.println("Failed to list files in directory.");  
        }  
    }  
  
    public static void main(String[] args) {  
        listFiles("目录路径"); // 目录路径  
    }  
}

检查目录是否存在

通过` File` 类的` exists()` 和` isDirectory()` 方法。
public static boolean checkDirectoryExists(String directoryPath) {      
      File directory = new File(directoryPath);   
      // 检查目录是否存在且为目录   
      return directory.exists() && directory.isDirectory();  
}  

创建目录

使用` File` 类的` mkdir()` 或` mkdirs()` 方法来创建目录。` mkdir()` 方法用于创建单个目录,而` mkdirs()` 方法用于创建目录的完整路径,如果上级目录不存在,则一并创建。
public static boolean createDirectory(String directoryPath) {  
    File directory = new File(directoryPath);  
    // 创建目录,包括不存在的父目录  
    return directory.mkdirs();  
    // 如果创建单个目录,使用 return directory.mkdir();  
}  

过滤文件

```java public class FileFilterExample {
public static void main(String[] args) {  
    // 过滤包含"example"的文件和目录  
    String filterString = "example";  

    // 获取当前目录下的所有文件和目录  
    File directory = new File(".");  
    File[] filesAndDirs = directory.listFiles(new FilenameFilter() {  
        @Override  
        public boolean accept(File dir, String name) {  
            // 这里可以添加任何过滤逻辑  
            // 例如,检查名称中是否包含特定的字符串  
            return name.contains(filterString);  
        }  
    });  

    // 打印过滤后的文件和目录  
    if (filesAndDirs != null) {  
        for (File file : filesAndDirs) {  
            System.out.println(file.getName()); // 打印文件名或目录名  
        }  
    } 
}  

}


<h2 id="O5mxX"><font style="color:#000000;">字符串分割</font></h2>
```java
public class StringSplitExample {  
    public static void main(String[] args) {  
        // 待分割的字符串  
        String strWithCustomDelimiter = "apple,banana,orange";  
        // 使用split()方法按逗号分割字符串  
        String[] parts = strWithCustomDelimiter.split(",");  
  
        // 使用for-each循环遍历分割后的字符串数组  
        for (String part : parts) {  
            System.out.println(part);  
        }  
  
        // 使用Streams API进行更复杂的操作  
        // 例如,将分割后的字符串转换为大写并打印  
        Stream.of(parts)  
            .map(String::toUpperCase)  
            .forEach(System.out::println);  
    }  
}

序列化

Java 序列化是一种将对象转换为字节流的过程,以便可以将对象保存到磁盘上,将其传输到网络上,或者将其存储在内存中,以后再进行反序列化,将字节流重新转换为对象。

序列化是一种用于保存、传输和还原对象的方法,它使得对象可以在不同的计算机之间移动和共享,这对于分布式系统、数据存储和跨平台通信非常有用。

import java.io.Serializable;

public class MyClass implements Serializable {
    // 类的成员和方法
}
// 创建一个MyClass的实例  
MyClass obj = new MyClass();  
  
// 尝试将对象序列化到文件中  
try {  
    // 创建一个文件用于写入数据  
    FileOutputStream fileOut = new FileOutputStream("object.ser");  
      
    // 创建一个ObjectOutputStream,它包装了fileOut  
    // ObjectOutputStream可以将Java对象写入底层OutputStream(在这里是fileOut)  
    // 这个过程称为序列化,即将对象的状态保存为一系列字节  
    ObjectOutputStream out = new ObjectOutputStream(fileOut);  
      
    // 调用writeObject方法,将obj对象序列化到文件中  
    out.writeObject(obj);  
      
    out.close();  // 关闭ObjectOutputStream,释放与该流关联的所有系统资源  
    fileOut.close(); // 显式关闭FileOutputStream  
      
} catch (IOException e) {  
    e.printStackTrace();  
}
MyClass obj = null;
try {
    FileInputStream fileIn = new FileInputStream("object.ser");
    ObjectInputStream in = new ObjectInputStream(fileIn);
    obj = (MyClass) in.readObject();
    in.close();
    fileIn.close();
} catch (IOException e) {
    e.printStackTrace();
} catch (ClassNotFoundException e) {
    e.printStackTrace();
}

第11章 运行期类型鉴定

RTTI

RTTI(Run-Time Type Identification,运行期类型鉴定)允许在程序运行时确定一个对象的实际类型。

我们会通过接口或父类来引用对象,这样可以利用多态性(Polymorphism)来编写灵活且可扩展的代码。

在某些情况下,我们可能需要根据对象的实际类型来执行特定的操作。这时,我们就需要用到RTTI来确定对象的确切类型。

Java中的RTTI实现方式

  1. 传统的RTTI
  • **<font style="color:#000000;">instanceof</font>**关键字:它在运行时检查一个对象是否是某个特定类的实例。
Shape shape = // 假设这是从某个地方获取的Shape对象  
if (shape instanceof Triangle) {  
    Triangle triangle = (Triangle) shape;  
}
  1. 反射(Reflection)

反射允许程序在运行时查询和操作类的属性、方法、构造函数等。

Class对象

每个类都有一个与之对应的` Class` 对象,这个对象包含了类的元数据信息,比如类的结构、方法、字段等。

Class对象的获取

Class<?> clazz = Class.forName("com.example.MyClass");
// 这是动态加载类并获取其Class对象的一种方式。
// className参数是类的完全限定名(包括包名)。
Class<MyClass> clazz = MyClass.class;
// 这是编译时安全的获取Class对象的方式。
// 它直接引用了类本身,因此不需要在运行时解析类名。
MyClass obj = new MyClass();  
Class<? extends MyClass> clazz = obj.getClass();
// 如果已经有一个类的实例,可以通过调用该实例的getClass()方法来获取其Class对象。

类的加载和初始化

当Java虚拟机(JVM)首次使用某个类时,会加载该类并初始化它。

加载是指将类的<font style="color:#000000;">.class</font>文件读入内存,并为之创建一个<font style="color:#000000;">Class</font>对象。

初始化则包括执行类的静态初始化块和初始化静态字段。

反射

假设有一个` Person` 类,想在不直接引用这个类的情况下,通过反射来创建它的实例、调用它的方法以及访问它的字段。
public class Person {  
    private String name;  
    private int age;  
  
    public Person(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }  
  
   ........
  
    public void greet() {  
        System.out.println("Hello, my name is " + name + " and I am " + 
                           age + " years old.");  
    }  
}
import java.lang.reflect.Constructor;  
import java.lang.reflect.Method;  
  
public class ReflectionExample {  
    public static void main(String[] args) {  
        try {  
            // 获取Person类的Class对象(使用完整包名)  
            Class<?> personClass = Class.forName("com.example.Person");  
  
            // 获取Person类的构造函数  
            Constructor<?> constructor = personClass.getConstructor(String.class, int.class);  
  
            // 通过构造函数创建Person类的实例  
            Object personInstance = constructor.newInstance("Alice", 30);  
  
            // 获取Person类的greet方法  
            Method greetMethod = personClass.getMethod("greet");  
  
            // 调用greet方法  
            greetMethod.invoke(personInstance);  
  
        } catch (Exception e) {  
            e.printStackTrace();  
        }  
    }  
}

一个类方法提取器

** 展示一个类(包括它的父类)的所有公共方法和构造函数。**
  • <font style="color:#000000;background-color:rgb(253, 253, 254);">getMethods()</font>返回类及其所有父类的所有<font style="color:#000000;background-color:rgb(253, 253, 254);">public</font>方法的一个数组;
  • <font style="color:#000000;background-color:rgb(253, 253, 254);">getConstructors()</font>返回类所有<font style="color:#000000;background-color:rgb(253, 253, 254);">public</font>构造函数的一个数组。
try {  
    Class c = Class.forName(args[0]); // 加载类  
    Method[] m = c.getMethods(); // 获取所有public方法  
    Constructor[] ctor = c.getConstructors(); // 获取所有public构造函数  
  
    // 遍历并打印方法  
    for (int i = 0; i < m.length; i++)  
        System.out.println(m[i].toString());  
  
    // 遍历并打印构造函数  
    for (int i = 0; i < ctor.length; i++)  
        System.out.println(ctor[i].toString());  
} catch (ClassNotFoundException e) {  
    System.out.println("No such class: " + e);  
}

第12章 传递和返回对象

12.1 传递句柄

将一个对象引用赋值给另一个引用变量时,这两个变量将指向堆内存中的同一个对象。这意味着,如果你通过其中一个引用修改了对象的状态,那么另一个引用看到的也将是修改后的对象。但是,如果你将其中一个引用重新赋值,使其指向另一个对象,这并不会影响另一个引用所指向的对象。

12.1.1 别名问题

Java中对象是通过引用来访问的,这些引用可以被赋值给多个变量。

当这些变量中的任何一个修改了对象的状态时,所有其他引用该对象的变量都会看到这些更改。

12.2 制作本地副本

■参数传递过程中会自动产生别名问题

■不存在本地对象,只有本地句柄

■句柄有自己的作用域,而对象没有

■对象的“存在时间”在Java里不是个问题

■没有语言上的支持(如常量)可防止对象被修改(以避免别名的副作用)

12.2.1 按值传递

按值传递:Java中的参数传递是按值传递的。

对于基本数据类型,传递的是值的副本。在方法内部对参数的修改不会影响原始变量。

对于对象类型,传递的是对象引用的副本(即句柄的副本)。这意味着方法内部接收到的引用和原始引用指向同一个对象。因此,在方法内部对对象状态的修改会反映到原始对象上。

12.2.2 克隆对象

Java本身不自动为对象创建本地副本,但可以通过手动复制对象的内容来创建本地副本。可以创建一个新的对象实例,并将原始对象的状态复制到新对象中。

这通常通过clone()方法实现:这是Object类中的一个受保护方法,用于创建对象的副本。在需要克隆的类中,需要覆盖这个方法并将其设为public。

class Int { // 定义一个简单的类来封装一个整数  
  private int i; // 私有成员变量i  
  public Int(int ii) {   
    i = ii;   
  }  
  public void increment() {   
    i++;   
  }  
  public String toString() {    
    return Integer.toString(i);    
  }  
}  
  
public class Cloning { 
  public static void main(String[] args) {   
    Vector v = new Vector(); // 创建一个Vector对象v  
  
    // 向v中添加10个Int对象,每个对象的i值从0到9  
    for(int i = 0; i < 10; i++ )   
      v.addElement(new Int(i));   
  
    // 打印v的内容,此时v中的Int对象的i值分别为0到9  
    System.out.println("v: " + v);   
  
    // 对v进行浅克隆,得到v2  
    Vector v2 = (Vector)v.clone();   
  
    // 遍历v2中的每个元素,并调用increment方法增加它们的i值  
for(Enumeration e = v2.elements(); e.hasMoreElements(); )  {  
    // elements()方法返回一个Enumeration对象,该对象包含了v2中所有元素的枚举。
    // 然后,这个Enumeration对象被赋值给变量e,以便在循环中使用。
    // e.hasMoreElements();)是循环的继续条件。
    // 在每次循环迭代之前,都会检查Enumeration对象e是否还有更多的元素。
    // 如果e中还有元素,hasMoreElements()方法将返回true,循环继续;
    // 如果没有更多元素,返回false,循环结束。
    ((Int)e.nextElement()).increment();  
}
    // 再次打印v的内容,由于v和v2中的Int对象是相同的对象(浅克隆),  
    // 所以v中的Int对象的i值也被修改了,它们的i值分别为1到10  
    System.out.println("v: " + v);   
  }  }

12.2.3 使类具有克隆能力

在Java中,` clone()` 方法是一个受保护的方法(protected),定义在 ` Object` 类中。这意味着默认情况下,你不能直接在类外部调用它,除非你的类覆盖了它并且改变了它的访问级别(通常是将其设为 ` public` )。此外,为了让一个类能够被克隆,该类必须实现 ` Cloneable` 接口,尽管这个接口不包含任何方法,但它作为一个标记接口存在,用于指示对象可以被克隆。
// 实现Cloneable接口  
public class Employee implements Cloneable {  
    private String name;  
    private int age;  
    // 构造函数  
    public Employee(String name, int age) {  
        this.name = name;  
        this.age = age;  
    }  
    // 覆盖clone()方法  
    @Override  
    public Object clone() throws CloneNotSupportedException {//一般设置为public
        // 调用super.clone()进行浅拷贝  
        return super.clone();  
    }  
  
    // 展示方法  
    public void display() {  
        System.out.println("Name: " + name + ", Age: " + age);  
    }  
}
public class CloneDemo {  
    public static void main(String[] args) {  
        try {  
            Employee emp1 = new Employee("John Doe", 30);  
            Employee emp2 = (Employee) emp1.clone(); // 进行克隆  
  
            // 修改emp2的属性,观察emp1是否受影响  
            emp2.setName("Jane Doe");  
            emp2.setAge(25);  
  
            System.out.println("Original Employee:");  
            emp1.display(); // 应显示 John Doe, 30  
  
            System.out.println("Cloned Employee:");  
            emp2.display(); // 应显示 Jane Doe, 25  
  
        } catch (CloneNotSupportedException e) {  
            e.printStackTrace();  
        }  }  }  

深拷贝与浅拷贝:在上面的例子中,<font style="color:#000000;">clone()</font> 方法实际上执行的是浅拷贝(shallow copy)。如果 <font style="color:#000000;">Employee</font> 类中包含了对其他对象的引用(比如另一个 <font style="color:#000000;">Address</font> 对象),则这些引用在克隆对象中仍会指向原始对象。如果需要深拷贝(deep copy),则需要在 <font style="color:#000000;">clone()</font> 方法中手动复制这些引用指向的对象。

12.2./..深层克隆

当我们在Java中进行深层复制时,特别是当对象包含其他对象的引用时,我们必须确保所有这些引用也都被适当地复制,以创建出一个完全独立的新对象结构。
// DepthReading类,只包含基本数据类型,可以简单地克隆  
class DepthReading implements Cloneable {  
    private double depth; // 深度数据  
  
    public DepthReading(double depth) {  this.depth = depth;  }  
  
    // 实现了Cloneable接口的clone方法,用于创建对象的浅层复制  
    // 但由于类内部只有基本数据类型,这里浅层复制就是深层复制  
    public Object clone() {  
        try {  
            return super.clone(); // 调用Object类的clone方法创建对象的副本  
        } catch (CloneNotSupportedException e) {  
            // 通常不会发生,因为类已经实现了Cloneable接口  
            e.printStackTrace();  
            return null; // 如果发生异常,返回null(这里更推荐抛出运行时异常)  
        }}}  
  
// TemperatureReading类,类似于DepthReading,也是基本数据类型的简单集合  
class TemperatureReading implements Cloneable {  
    private long time; // 记录时间  
    private double temperature; // 温度数据  
  
    public TemperatureReading(double temperature) {  
        this.time = System.currentTimeMillis(); // 记录当前时间  
        this.temperature = temperature;  }  
  
    // 实现了Cloneable接口的clone方法,同样这里浅层复制就足够了  
    public Object clone() {  
        try {  
            return super.clone();  
        } catch (CloneNotSupportedException e) {  
            e.printStackTrace();  
            return null;  }}}  
  
// OceanReading类,包含对其他对象的引用,需要进行深层复制  
class OceanReading implements Cloneable {  
    private DepthReading depth; // 深度读数  
    private TemperatureReading temperature; // 温度读数  
  
    public OceanReading(double tdata, double ddata) {  
        this.temperature = new TemperatureReading(tdata);  
        this.depth = new DepthReading(ddata);  }  
  
    // 实现了Cloneable接口的clone方法,用于创建对象的深层复制  
    public Object clone() {  
        OceanReading o = null;  
        try {  
            o = (OceanReading) super.clone(); // 首先调用super.clone()进行浅层复制  
            // 然后手动复制内部的DepthReading和TemperatureReading对象  
            // 这些对象也必须支持克隆(即实现Cloneable接口并重写clone方法)  
            o.depth = (DepthReading) depth.clone();  
            o.temperature = (TemperatureReading) temperature.clone();  
  
        } catch (CloneNotSupportedException e) {  
            e.printStackTrace();  
        }  
        return o; // 返回深层复制后的对象  
    }  
}  
  
public class DeepCopy {  
    public static void main(String[] args) {  
        OceanReading reading = new OceanReading(33.9, 100.5);  
        // 现在克隆它  
        OceanReading r = (OceanReading) reading.clone();  
        // r是reading的一个深层复制,对r的修改不会影响reading  } }

12.2.7 用Vector 进行深层复制

```java // 定义一个可克隆的Int2类 class Int2 implements Cloneable { private int i; // 私有成员变量 // 构造函数 public Int2(int ii) { i = ii; } // 增加i的值 public void increment() { i++; } // 实现克隆方法 public Object clone() { Object o = null; try { // 调用Object类的clone方法,实现浅复制 o = super.clone(); } catch (CloneNotSupportedException e) { // 如果当前类不被允许克隆,则打印错误消息 System.out.println("Int2 can't clone"); } return o; } }

// Int3继承自Int2,并添加了一个新的私有成员变量j
// 由于Int2已经实现了Cloneable接口并覆盖了clone方法,Int3也自动继承了这些特性
class Int3 extends Int2 {
private int j; // 自动被复制,因为Object.clone()会复制所有二进制位
// 构造函数
public Int3(int i) { super(i); }
// 这里不需要再次覆盖clone(),因为Object.clone()已经足够
}

public class AddingClone {
public static void main(String[] args) {
// 测试Int2的克隆
Int2 x = new Int2(10);
Int2 x2 = (Int2)x.clone();
x2.increment();
System.out.println(
"x = " + x + ", x2 = " + x2); // 输出显示x和x2是独立的对象
// 测试继承的克隆能力
Int3 x3 = new Int3(7);
x3 = (Int3)x3.clone(); // Int3也可以克隆

// 创建一个Vector,并填充Int2对象  
Vector v = new Vector();   
for(int i = 0; i < 10; i++ )   
  v.addElement(new Int2(i));   
System.out.println("v: " + v);  
  
// 对Vector进行浅复制  
Vector v2 = (Vector)v.clone();  
  
// 对v2中的每个元素进行深层复制  
for(int i = 0; i < v.size(); i++)   
  v2.setElementAt(  
    ((Int2)v2.elementAt(i)).clone(), i);  
  
// 修改v2中所有元素的值  
for(Enumeration e = v2.elements();   
    e.hasMoreElements(); )   
  ((Int2)e.nextElement()).increment();  
  
// 验证v和v2的内容是否独立  
System.out.println("v: " + v); // v的内容未变  
System.out.println("v2: " + v2); // v2的内容已变  

}
}


<font style="color:#000000;"> 对Vector进行深层复制的先决条件:在克隆了Vector后,必须在其中遍历,并克隆 由Vector指向的每个对象。  </font>

<h3 id="JbEz9"><font style="color:#000000;"> 12.2.8 通过序列化进行深层复制  </font></h3>
```java
// 一个简单的可序列化类  
class Thing1 implements Serializable {}   
  
// 包含另一个可序列化对象的类  
class Thing2 implements Serializable {   
  Thing1 o1 = new Thing1(); // Thing2包含一个Thing1对象  
}   
  
// 一个实现了Cloneable接口的类,但这里没有使用任何特定的序列化或反序列化机制  
class Thing3 implements Cloneable {   
  public Object clone() {   
    Object o = null;   
    try {   
      o = super.clone(); // 调用Object类的clone()方法  
    } catch (CloneNotSupportedException e) {   
      System.out.println("Thing3 can't clone"); // 理论上,实现了Cloneable接口的类不会抛出此异常  
    }   
    return o;   
  }   
}   
  
// 一个实现了Cloneable接口的类,并且正确地复制了其包含的对象  
class Thing4 implements Cloneable {   
  Thing3 o3 = new Thing3(); // Thing4包含一个Thing3对象  
  public Object clone() {   
    Thing4 o = null;   
    try {   
      o = (Thing4)super.clone(); // 浅复制Thing4  
    } catch (CloneNotSupportedException e) {   
      System.out.println("Thing4 can't clone");   
    }   
    // 深层复制Thing4中的Thing3对象  
    o.o3 = (Thing3)o3.clone();   
    return o;   
  }   
}   
  
public class Compete {   
  static final int SIZE = 5000; // 定义数组大小  
  
  public static void main(String[] args) {   
    // 创建并初始化Thing2对象数组  
    Thing2[] a = new Thing2[SIZE];  
    for(int i = 0; i < a.length; i++)   
      a[i] = new Thing2();  
  
    // 创建并初始化Thing4对象数组  
    Thing4[] b = new Thing4[SIZE];  
    for(int i = 0; i < b.length; i++)   
      b[i] = new Thing4();  
  
    try {   
      // 使用序列化进行复制  
      long t1 = System.currentTimeMillis(); // 记录开始时间  
      ByteArrayOutputStream buf = new ByteArrayOutputStream();  
      ObjectOutputStream o = new ObjectOutputStream(buf);  
      for(int i = 0; i < a.length; i++)   o.writeObject(a[i]);  
        
      // 反序列化获取复制的对象  
      ObjectInputStream in = new ObjectInputStream(new ByteArrayInputStream(buf.toByteArray()));  
      Thing2[] c = new Thing2[SIZE];  
      for(int i = 0; i < c.length; i++)  
        c[i] = (Thing2)in.readObject();  
        
      long t2 = System.currentTimeMillis(); // 记录结束时间  
      System.out.println("Duplication via serialization: " + (t2 - t1) + " Milliseconds");  
  
      // 使用克隆进行复制  
      t1 = System.currentTimeMillis(); // 重新记录开始时间  
      Thing4[] d = new Thing4[SIZE];  
      for(int i = 0; i < d.length; i++)   
        d[i] = (Thing4)b[i].clone();  
        
      t2 = System.currentTimeMillis(); // 记录结束时间  
      System.out.println("Duplication via cloning: " + (t2 - t1) + " Milliseconds");  
    } catch(Exception e) {   
      e.printStackTrace(); // 捕获并打印异常信息  
    }   
  }   
}
  • 序列化复制:涉及大量的I/O操作,包括磁盘I/O或内存中的字节数组操作(如本例所示)。这些操作相对较慢,且结果受JVM实现和当前系统负载的影响,因此运行时间不稳定。
  • 克隆复制:这种复制发生在JVM内部,不涉及I/O操作,因此速度更快且更稳定。

使用序列化进行复制的时间远远长于使用克隆进行复制的时间。

12.3 克隆的控制

```java // CheckCloneable.java // 检查一个对象是否可以克隆

// Ordinary 类不支持克隆,但也没有禁止克隆
class Ordinary {}

// WrongClone 类错误地实现了克隆:覆盖了 clone(),但没有实现 Cloneable
// implements Cloneable
class WrongClone extends Ordinary {
public Object clone() throws CloneNotSupportedException {
return super.clone(); // 因为没有实现 Cloneable,所以会抛出 CloneNotSupportedException
}
}

// IsCloneable 类正确地实现了克隆:覆盖了 clone(),并实现了 Cloneable
class IsCloneable extends Ordinary implements Cloneable {
public Object clone() throws CloneNotSupportedException {
return super.clone(); // 调用 Object 的 clone(),并捕获可能抛出的异常(但这里没有捕获)
}
}

// NoMore 类试图通过抛出异常来“关闭”克隆
class NoMore extends IsCloneable {
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException(); // 显式抛出异常来禁止克隆
}
}

// TryMore 类继承了 NoMore,并调用了 super.clone(),因此也会抛出异常
class TryMore extends NoMore {
public Object clone() throws CloneNotSupportedException {
// 调用 NoMore.clone(),后者会抛出异常
return super.clone();
}
}

// BackOn 类展示了如何绕过 NoMore 的克隆限制
class BackOn extends NoMore {
private BackOn duplicate(BackOn b) {
// 创建一个新的 BackOn 实例作为副本(这里只是示例,实际复制逻辑会更复杂)
return new BackOn();
}

public Object clone() {
// 不调用 super.clone(),而是使用自定义的 duplicate 方法来创建副本
return duplicate(this);
}
}

// ReallyNoMore 类被设为 final,以防止继承,从而彻底禁止克隆
final class ReallyNoMore extends NoMore {}

public class CheckCloneable {
// 尝试克隆任何 Ordinary 类型的对象
static Ordinary tryToClone(Ordinary ord) {
String id = ord.getClass().getName();
Ordinary x = null;
if (ord instanceof Cloneable) {
try {
System.out.println("Attempting " + id);
// 尝试将 ord 造型为 IsCloneable 并调用 clone(),注意这里假设了所有 Cloneable 对象都是 IsCloneable 类型
x = (Ordinary)((IsCloneable)ord).clone();
System.out.println("Cloned " + id);
} catch (CloneNotSupportedException e) {
System.out.println("Could not clone " + id);
}
}
return x;
}

public static void main(String[] args) {
// 创建一个包含不同类型 Ordinary 对象的数组,并上溯造型为 Ordinary
Ordinary[] ord = {
new IsCloneable(),
new WrongClone(),
new NoMore(),
new TryMore(),
new BackOn(),
new ReallyNoMore(),
};

// 尝试克隆一个纯粹的 Ordinary 对象(不会编译通过,因为 clone() 是 protected 的)  
// Ordinary x = new Ordinary();  
// x = (Ordinary)x.clone(); // 这行代码不会编译  
  
// 遍历数组并尝试克隆每个对象  
for (int i = 0; i < ord.length; i++)  
  tryToClone(ord[i]);  

}
}


1. **<font style="color:#000000;">WrongClone 类</font>**<font style="color:#000000;">:尽管它覆盖了</font><font style="color:#000000;"> </font>`<font style="color:#000000;">clone()</font>`<font style="color:#000000;"> </font><font style="color:#000000;">方法,但由于没有实现</font><font style="color:#000000;"> </font>`<font style="color:#000000;">Cloneable</font>`<font style="color:#000000;"> </font><font style="color:#000000;">接口,调用</font><font style="color:#000000;"> </font>`<font style="color:#000000;">super.clone()</font>`<font style="color:#000000;"> </font><font style="color:#000000;">时会抛出</font><font style="color:#000000;"> </font>`<font style="color:#000000;">CloneNotSupportedException</font>`<font style="color:#000000;">。</font>
2. **<font style="color:#000000;">IsCloneable 类</font>**<font style="color:#000000;">:正确地实现了克隆,覆盖了</font><font style="color:#000000;"> </font>`<font style="color:#000000;">clone()</font>`<font style="color:#000000;"> </font><font style="color:#000000;">方法并实现了</font><font style="color:#000000;"> </font>`<font style="color:#000000;">Cloneable</font>`<font style="color:#000000;"> </font><font style="color:#000000;">接口。然而,它没有捕获</font><font style="color:#000000;"> </font>`<font style="color:#000000;">CloneNotSupportedException</font>`<font style="color:#000000;">(在这个例子中,由于实现了</font><font style="color:#000000;"> </font>`<font style="color:#000000;">Cloneable</font>`<font style="color:#000000;">,这个异常实际上不会被抛出)。</font>
3. **<font style="color:#000000;">NoMore 和 TryMore 类</font>**<font style="color:#000000;">:通过显式抛出</font><font style="color:#000000;"> </font>`<font style="color:#000000;">CloneNotSupportedException</font>`<font style="color:#000000;"> </font><font style="color:#000000;">来禁止克隆。</font>`<font style="color:#000000;">TryMore</font>`<font style="color:#000000;"> </font><font style="color:#000000;">继承了</font><font style="color:#000000;"> </font>`<font style="color:#000000;">NoMore</font>`<font style="color:#000000;">,因此其行为与</font><font style="color:#000000;"> </font>`<font style="color:#000000;">NoMore</font>`<font style="color:#000000;"> </font><font style="color:#000000;">相同。</font>
4. **<font style="color:#000000;">BackOn 类</font>**<font style="color:#000000;">:展示了如何绕过继承链中的克隆限制,通过不调用</font><font style="color:#000000;"> </font>`<font style="color:#000000;">super.clone()</font>`<font style="color:#000000;"> </font><font style="color:#000000;">并实现自己的克隆逻辑。</font>
5. **<font style="color:#000000;">ReallyNoMore 类</font>**<font style="color:#000000;">:通过将类设为</font><font style="color:#000000;"> </font>`<font style="color:#000000;">final</font>`<font style="color:#000000;">,防止了进一步的继承,从而彻底禁止了克隆。</font>
6. **<font style="color:#000000;">tryToClone 方法</font>**<font style="color:#000000;">:尝试克隆任何</font><font style="color:#000000;"> </font>`<font style="color:#000000;">Ordinary</font>`<font style="color:#000000;"> </font><font style="color:#000000;">类型的对象,但假设所有可克隆的对象都是</font><font style="color:#000000;"> </font>`<font style="color:#000000;">IsCloneable</font>`<font style="color:#000000;"> </font><font style="color:#000000;">的类型(这在实际应用中可能不是一个好假设)。</font>
7. **<font style="color:#000000;">main 方法</font>**<font style="color:#000000;">:创建了一个包含不同类型对象的数组,并尝试克隆每个对象。通过输出可以看到哪些对象可以被克隆,哪些不能。</font>

<h2 id="ctRGE"><font style="color:#000000;"> 12.4 只读类  </font></h2>
<font style="color:#000000;">允许只读, 其中没有任何方法能造成对象 内部状态的改变。  </font>

<h3 id="cWNm0"><font style="color:#000000;"> 12.4.1 创建只读类  </font></h3>
```java
public class Immutable1 {  
  // 私有字段,确保外部无法直接访问和修改  
  private int data;  
  
  // 构造函数,用于初始化对象  
  public Immutable1(int initVal) {  
    data = initVal; // 内部设置data的值  
  }  
  
  // 公开方法,用于读取data的值  
  public int read() {  
    return data; // 返回data的值,但不提供修改途径  
  }  
  
  // 公开方法,检查data是否不为0  
  public boolean nonzero() {  
    return data != 0; // 返回data是否不等于0的布尔值  
  }  
  
  // 公开方法,生成并返回当前对象数据值的四倍的新Immutable1对象  
  // 注意:这里没有修改当前对象,而是创建了一个新的对象  
  public Immutable1 quadruple() {  
    return new Immutable1(data * 4); // 创建一个新的Immutable1对象,其data是原对象data的四倍  
  }  
  
  // 静态方法,演示如何对Immutable1对象进行操作  
  static void f(Immutable1 i1) {  
    // 调用quadruple()方法获取一个新的Immutable1对象,其data是i1的data的四倍  
    Immutable1 quad = i1.quadruple();  
    // 打印原对象i1的data值  
    System.out.println("i1 = " + i1.read());  
    // 打印新对象quad的data值  
    System.out.println("quad = " + quad.read());  
  }  
  
  // 主方法,程序的入口点  
  public static void main(String[] args) {  
    // 创建一个Immutable1对象x,其data值为47  
    Immutable1 x = new Immutable1(47);  
    // 打印x的data值  
    System.out.println("x = " + x.read());  
    // 调用f方法,传入x作为参数  
    f(x);  
    // 再次打印x的data值,以证明x没有被修改  
    System.out.println("x = " + x.read()); // 输出应与第一次相同,因为x是不可变的  
  }  
}

这个类通过将所有字段设为<font style="color:#000000;">private</font>来确保外部代码无法直接访问或修改这些字段。如果需要修改数据,则必须创建一个新的对象,如<font style="color:#000000;">quadruple()</font>方法所示,这种方法保证了对象的不可变性。

12.4.3 不变字串

** 1. 字符串的不可变性**

在Java中,字符串是不可变的。当你调用任何修改字符串的方法时,实际上并不是在修改原始字符串对象,而是创建并返回了一个新的字符串对象。这意味着原始字符串对象保持不变,从而避免了因多个引用指向同一对象而导致的潜在问题。

2. 隐式常数

在Java中,字符串常量在编译时会被自动放入字符串常量池中。如果程序中出现了多个相同的字符串常量,它们实际上会指向常量池中的同一个对象。这有助于提高内存效率和性能。然而,对于通过<font style="color:#000000;">new String()</font>创建的字符串对象,即使它们的内容相同,也不会共享相同的对象引用。

3. 覆盖"+" 和 StringBuffer

在Java中,字符串的"+“操作符被重载以支持字符串的连接。然而,每次使用”+"连接字符串时,实际上都会创建一个新的字符串对象,这可能会导致大量的中间对象被创建和丢弃,从而影响性能。为了解决这个问题,Java提供了<font style="color:#000000;">StringBuffer</font><font style="color:#000000;">StringBuilder</font>类,它们是可变的字符串类,可以在不创建新对象的情况下修改字符串。

在处理大量字符串连接时,显式使用<font style="color:#000000;">StringBuilder</font><font style="color:#000000;">StringBuffer</font>通常是一个更好的选择。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值