uacs2024

导航

Java基础补缺2

1)多线程调度:

每个对象都可以调用 Object 的 wait/notify 方法来实现等待/通知机制。

public class WaitNotifyDemo {
    public static void main(String[] args) {
        Object lock = new Object();
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程1:我要等待");
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("线程1:我被唤醒了");
            }
        }).start();
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("线程2:我要唤醒");
                lock.notify();
                System.out.println("线程2:我已经唤醒了");
            }
        }).start();
    }
}

在大多数情况下,由于线程1先被启动,它会先获得锁。以下是详细的执行序列:

  1. 主线程启动:
    • 主线程创建了一个共享对象 lock作为监视器锁。
    • 主线程启动线程1。线程1进入 Runnable状态,等待CPU调度。
    • 主线程立即启动线程2。线程2也进入 Runnable状态。
  2. 线程1获得锁并进入等待:
    • 线程1获得CPU时间片,并成功获取 lock上的同步锁,进入 synchronized代码块。
    • 线程1打印:"线程1:我要等待"
    • 线程1执行 lock.wait()。这个方法调用会导致以下关键操作:
      • 线程1释放其持有的 lock锁。
      • 线程1的状态由 RUNNABLE变为 WAITING,并被放入与 lock对象关联的等待队列中。
    • 此时,lock的锁被释放,其他线程可以竞争它。
  3. 线程2获得锁并发出通知:线程1被唤醒并继续执行:
    • 线程2获得CPU时间片,并成功获取到已空闲的 lock锁,进入其 synchronized代码块。
    • 线程2打印:"线程2:我要唤醒"
    • 线程2执行 lock.notify()。这个方法调用会引发以下操作:
      • JVM将线程1从 lock的等待队列移送到同步队列(或锁的阻塞队列)。线程1的状态由 WAITING变为 BLOCKED,因为它现在需要重新竞争锁才能继续执行。
      • 请注意:notify()调用本身并不会释放锁,线程2会继续持有锁并执行后续代码。
    • 线程2打印:"线程2:我已经唤醒了"
    • 线程2执行到其 synchronized代码块的结束处 },这时它会释放​ lock锁。
    • lock锁被释放后,在同步队列中等待的线程1开始尝试重新获取该锁。
    • 线程1成功获取锁后,从其 wait()方法处恢复执行。
    • 线程1打印:"线程1:我被唤醒了"
    • 线程1执行到其 synchronized代码块的结束处 },释放锁。程序随后结束。
线程1:我要等待
线程2:我要唤醒
线程2:我已经唤醒了
线程1:我被唤醒了

潜在风险:竞态条件(Race Condition)

这段代码存在一个典型的竞态条件问题。如果线程调度器让线程2先于线程1执行,将会发生以下情况:

  1. 线程2先获得 lock锁。
  2. 线程2调用 lock.notify()。但此时,线程1尚未执行到 wait()方法,即等待队列是空的。因此,这次 notify()调用相当于一次空操作,没有任何线程会被唤醒。
  3. 线程2执行完毕,释放锁。
  4. 之后,线程1获得锁,执行 lock.wait()并进入等待状态。
  5. 由于已经没有任何其他线程会再次调用 lock.notify(),线程1将永远等待下去,程序无法正常终止。

 

2)Java包

Java 定义了一种名字空间,称之为包:package。一个类总是属于某个包,类名(比如Person)只是一个简写,真正的完整类名是包名.类名

在 Java 虚拟机执行的时候,JVM 只看完整类名,因此,只要包名不同,类就不同。

要特别注意:包没有父子关系。java.util和java.util.zip是不同的包,两者没有任何继承关系。

 

在写import的时候,可以使用*,表示把这个包下面的所有class都导入进来(但不包括子包的class)。我们一般不推荐这种写法,因为在导入了多个包后,很难看出Arrays类属于哪个包。

 

还有一种import static的语法,它可以导入一个类的静态字段和静态方法。import static很少使用。

package main;

// 导入System类的所有静态字段和静态方法:
import static java.lang.System.*;

public class Main {
    public static void main(String[] args) {
        // 相当于调用System.out.println(…)
        out.println("Hello, world!");
    }
}

 

Java 编译器最终编译出的.class文件只使用 完整类名,因此,在代码中,当编译器遇到一个class名称时:

  • 如果是完整类名,就直接根据完整类名查找这个class
  • 如果是简单类名,按下面的顺序依次查找:
    • 查找当前package是否存在这个class
    • 查找import的包是否包含这个class
    • 查找java.lang包是否包含这个class

如果按照上面的规则还无法确定类名,则编译报错。

编写 class 的时候,编译器会自动帮我们做两个 import 动作:

  • 默认自动import当前package的其他class
  • 默认自动import java.lang.*

注意:自动导入的是java.lang包,但类似java.lang.reflect这些包仍需要手动导入。

如果有两个class名称相同,例如,mr.jun.Arraysjava.util.Arrays,那么只能import其中一个,另一个必须写完整类名。

 

3)可变参数

可变参数是 Java 1.5 的时候引入的功能,它允许方法使用任意多个、类型相同(is-a)的值作为参数。

 

public static void print(String... strs) {
    for (String s : strs)
        System.out.print(s);
    System.out.println();
}

 

阿里巴巴开发手册一条规约,尽量不要使用可变参数,如果要用的话,可变参数必须要在参数列表的最后一位

 

当使用可变参数的时候,实际上是先创建了一个数组,该数组的大小就是可变参数的个数,然后将参数放入数组当中,再将数组传递给被调用的方法。

 

public static void main(String[] args) {
    print(new String[]{"沉"});
    print(new String[]{"沉", "默"});
    print(new String[]{"沉", "默", "王"});
    print(new String[]{"沉", "默", "王", "二"});
}

public static void print(String... strs) {
    for (String s : strs)
        System.out.print(s);
    System.out.println();
}

 

当一个方法需要处理任意多个相同类型的对象时,就可以定义可变参数。Java 中有一个很好的例子,就是 String 类的 format() 方法

%d 表示将整数格式化为 10 进制整数,%s 表示输出字符串。

 

 

System.out.println(String.format("年纪是: %d", 18));
System.out.println(String.format("年纪是: %d 名字是: %s", 18, "沉默王二"));

 

要避免重载带有可变参数的方法——这样很容易让编译器陷入自我怀疑中

 

 

4)构造方法

构造方法不能是抽象的(abstract)、静态的(static)、最终的(final)、同步的(synchronized)。多个线程不会同时创建内存地址相同的同一个对象,所以用 synchronized 关键字修饰没有必要。

如果用 void 声明构造方法的话,编译时不会报错,但 Java 会把这个所谓的“构造方法”当成普通方法来处理

构造方法的调用是隐式的,通过编译器完成。如果没有明确提供无参构造方法,编译器会提供。而普通方法任何情况下都不由编译器提供。

 

复制对象有几种方法

4.1)通过构造函数

public class CopyConstrutorPerson {
    private String name;
    private int age;

    public CopyConstrutorPerson(CopyConstrutorPerson person) {
        this.name = person.name;
        this.age = person.age;
    }
public static void main(String[] args) {
        CopyConstrutorPerson p1 = new CopyConstrutorPerson("沉默王二",18);
        p1.out();

        CopyConstrutorPerson p2 = new CopyConstrutorPerson(p1);
        p2.out();
    }
}

4.2)通过Object类的clone()方法

public class ClonePerson implements Cloneable {
    private String name;
    private int age;

    public ClonePerson(String name, int age) {
        this.name = name;
        this.age = age;
    }

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

    public void out() {
        System.out.println("姓名 " + name + " 年龄 " + age);
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        ClonePerson p1 = new ClonePerson("沉默王二",18);
        p1.out();

        ClonePerson p2 = (ClonePerson) p1.clone();
        p2.out();
    }
}

通过 clone() 方法复制对象的时候,ClonePerson 必须先实现 Cloneable 接口的 clone() 方法,然后再调用 clone() 方法(ClonePerson p2 = (ClonePerson) p1.clone())。

 

 

5)初始代码块

public class Car {
    Car() {
        System.out.println("构造方法");
    }

    {
        System.out.println("代码初始化块");
    }

    public static void main(String[] args) {
        new Car();
    }
}

等价于

public class Car {
    Car() {
        {
            System.out.println("代码初始化块");
        }
        System.out.println("构造方法");
    }
}

结果

代码初始化块
构造方法
  • 类实例化的时候执行代码初始化块;
  • 实际上,代码初始化块是放在构造方法中执行的,只不过比较靠前;
  • 代码初始化块里的执行顺序是从前到后的。

 

在默认情况下,子类的构造方法在执行的时候会主动去调用父类的构造方法。

 

public class Example {
    // 静态变量
    public static int staticVar = 1;
    // 实例变量
    public int instanceVar = 2;

    // 静态初始化块
    static {
        System.out.println("执行静态初始化块");
        staticVar = 3;
    }

    // 实例初始化块
    {
        System.out.println("执行实例初始化块");
        instanceVar = 4;
    }

    // 构造方法
    public Example() {
        System.out.println("执行构造方法");
    }

    public static void main(String[] args) {
        System.out.println("执行main方法");

        Example e1 = new Example();
        Example e2 = new Example();

        System.out.println("e1的静态变量:" + e1.staticVar);
        System.out.println("e1的实例变量:" + e1.instanceVar);
        System.out.println("e2的静态变量:" + e2.staticVar);
        System.out.println("e2的实例变量:" + e2.instanceVar);
    }
}

结果

执行静态初始化块
执行main方法
执行实例初始化块
执行构造方法
执行实例初始化块
执行构造方法
e1的静态变量:3
e1的实例变量:4
e2的静态变量:3
e2的实例变量:4

静态初始化块在类加载时执行,只会执行一次,并且优先于实例初始化块和构造方法的执行

实例初始化块在每次创建对象时执行,在构造方法之前执行。

posted on 2025-12-11 21:48  ᶜʸᵃⁿ  阅读(2)  评论(0)    收藏  举报