2023年Java核心技术面试第八篇(篇篇万字精讲)

目录

十五 . 面向对象的基本要素:封装,继承,多态

 15.1 封装:

15.1.1 例子:

15.2 继承

15.2.1 例子

15.3 多态

15.3.1 例子

15.3.2 小结:谈谈多态的继承的联系

十六 . synchronized 和 ReentrantLock 的区别?

16.1 典型回答

16.2 深入理解底层锁的概念

16.2.1 synchronized

16.2.2 ReentrantLock

16.2.2.1 例子:

16.2.2.1.1 公平性(Fairness)控制:

16.2.2.1.2 可中断性(Interruptibility):

16.2.2.1.3 条件变量(Condition)支持:

16.2.2.1.4 超时控制(Timeout):


十五 . 面向对象的基本要素:封装,继承,多态

 15.1 封装:

封装:封装是将数据和功能包装在一个类中,通过对外提供公共接口来隐藏内部实现细节。这样可以保护数据免受外部直接访问和修改,只能通过类提供的方法进行操作,封装提供了数据的安全性和代码的可维护性。

15.1.1 例子:

我们可以创建一个名为"Person"的类来封装一个人的相关信息,如姓名、年龄和性别。通过定义公共方法如"setName"、"setAge"和"setGender"来设置这些属性值,而不直接暴露给外部代码。这样可以确保属性值的正确性和一致性。

public class Person {
    private String name;
    private int age;
    private String gender;

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

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public String getGender() {
        return gender;
    }

    public void setGender(String gender) {
        this.gender = gender;
    }

    public static void main(String[] args) {
        Person person = new Person("John", 25, "Male");
        System.out.println("Person Name: " + person.getName());
        System.out.println("Person Age: " + person.getAge());
        System.out.println("Person Gender: " + person.getGender());

        // 修改属性值
        person.setName("Alice");
        person.setAge(30);
        person.setGender("Female");

        System.out.println("Updated Person Name: " + person.getName());
        System.out.println("Updated Person Age: " + person.getAge());
        System.out.println("Updated Person Gender: " + person.getGender());
    }
}

15.2 继承

继承:继承是指一个类(称为子类或派生类)可以继承另一个类(称为父类或基类)的属性和方法。

子类继承了父类的特性,并且可以在此基础上进行扩展或重写。继承实现了代码的重用和扩展性。

15.2.1 例子

我们可以定义一个"Animal"类作为父类,其中包含通用的属性和方法,如"eat"和"sleep"。然后可以创建子类如"Cat"和"Dog"来继承"Animal"类,并在子类中添加特定的属性和方法,如"meow"方法和"bark"方法。

// 定义父类 Animal
class Animal {
    // 父类的属性
    protected String name;
  
    // 父类的方法
    public Animal(String name) {
        this.name = name;
    }
  
    public void eat() {
        System.out.println(name + " is eating.");
    }
  
    public void sleep() {
        System.out.println(name + " is sleeping.");
    }
}

// 定义子类 Cat
class Cat extends Animal {
    // 子类的属性
    private String breed;
  
    // 子类的方法,并调用父类的构造方法
    public Cat(String name, String breed) {
        super(name); // 调用父类的构造方法
        this.breed = breed;
    }
  
    // 子类的自定义方法
    public void meow() {
        System.out.println(name + " is meowing.");
    }
}

// 定义子类 Dog
class Dog extends Animal {
    // 子类的属性
    private String breed;
  
    // 子类的方法,并调用父类的构造方法
    public Dog(String name, String breed) {
        super(name); // 调用父类的构造方法
        this.breed = breed;
    }
  
    // 子类的自定义方法
    public void bark() {
        System.out.println(name + " is barking.");
    }
}

// 测试代码
public class Main {
    public static void main(String[] args) {
        Cat cat = new Cat("Kitty", "Persian");
        cat.eat(); // 调用继承自父类的方法
        cat.sleep();
        cat.meow(); // 调用子类自定义的方法

        Dog dog = new Dog("Buddy", "Labrador");
        dog.eat(); // 调用继承自父类的方法
        dog.sleep();
        dog.bark(); // 调用子类自定义的方法
    }
}

输出:

Kitty is eating.
Kitty is sleeping.
Kitty is meowing.
Buddy is eating.
Buddy is sleeping.
Buddy is barking.

15.3 多态

多态:多态是指同一种操作可以作用于不同的对象,并根据对象的实际类型执行不同的行为。通过多态,可以提高代码的灵活性和可扩展性。

15.3.1 例子

定义了一个抽象类 Animal,并具有一个抽象方法 sound。然后,定义了两个子类 CatDog,它们分别继承自 Animal 并实现了 sound 方法。

在测试类中,创建了一个 Animal 类型的对象数组,并分别用 CatDog 的实例来初始化数组的元素。然后通过循环遍历数组,并调用 sound 方法,可以看到根据对象的实际类型,程序会执行不同的行为。这就是多态的体现,相同的方法名 sound 可以作用于不同的对象,并根据对象的实际类型执行不同的行为。

// 定义父类 Animal
abstract class Animal {
    abstract void sound();
}

// 定义子类 Cat
class Cat extends Animal {
    void sound() {
        System.out.println("喵喵喵");
    }
}

// 定义子类 Dog
class Dog extends Animal {
    void sound() {
        System.out.println("汪汪汪");
    }
}

// 测试类
public class PolymorphismExample {
    public static void main(String[] args) {
        // 创建 Animal 对象数组
        Animal[] animals = new Animal[2];
        animals[0] = new Cat();
        animals[1] = new Dog();

        // 循环遍历数组并调用 sound 方法
        for (Animal animal : animals) {
            animal.sound();
        }
    }
}

15.3.2 小结:谈谈多态的继承的联系

继承是建立类之间的一种层次关系,子类可以继承父类的属性和方法,并且可以增加自己的特定实现;而多态是在继承关系中,通过父类的类型引用来指向子类的对象,并且根据对象的实际类型执行不同的行为。继承是一种静态的关系,而多态是一种动态的行为。继承和多态通常是一起使用,多态是继承的一种体现。

十六 . synchronized 和 ReentrantLock 的区别?

 Java精心设计的高效并发机制,构建大规模应用的基础之一。

16.1 典型回答

synchronized 是Java内建的同步机制,也被人称作Intrinsic Locking,提供互斥的语义和可见性,当一个线程已经获取当前锁时,其他试图获取的线程只能等待或者阻塞在哪里。

Java 5 以前,synchronized 是仅有的同步手段,在代码中,synchronized可以用来修饰方法,可以在特定的代码快上,本质上synchronized方法,等同于把方法全部语句用synchronized块包起来。

ReentrantLock ,通常称为再入锁,Java提供锁的实现,语义和synchronized差不多,再入锁可以直接通过代码,直接调用Lock()方法获取,代码书写更加灵活,ReentrantLock提供了很多实用的方法,实现了很多synchronized无法做到的细节控制,可以控制fairness,也就是公平性,或者利用定义条件等,明确调用unlock()方法释放,不然就会一直持有这个锁。

synchronized和ReentrantLock的性能比较,早期synchronized再很多场景下性能相差比较大,后续进行改进后,再低竞争的场景表现可能优于ReentrantLock。

16.2 深入理解底层锁的概念

16.2.1 synchronized

16.2.2 ReentrantLock

ReentrantLock是Java提供的可重入锁实现,它具有更细粒度的控制和更多的功能。相较于synchronizedReentrantLock提供了以下优势:

  1. 公平性(Fairness)控制:ReentrantLock可以通过构造方法的参数来指定是否按照线程请求锁的顺序获取锁(公平性),以避免某些线程长时间等待锁而产生饥饿现象。
  2. 可中断性(Interruptibility):ReentrantLock提供了可中断的获取锁的方法,即线程在等待锁的过程中可以被其他线程中断,并通过捕获InterruptedException来处理中断事件。
  3. 条件变量(Condition)支持:ReentrantLock内置了Condition接口,可以创建多个条件变量,使线程能够在特定条件满足时等待或被唤醒,从而实现更灵活的线程协作。
  4. 超时控制(Timeout):ReentrantLock提供了尝试获取锁的方法,可以指定一个超时时间,在超过指定时间后如果无法获得锁,则继续执行其他操作,避免线程长时间等待。

在高竞争的多线程场景下,ReentrantLock通常表现更好,因为它提供了对锁的更细粒度的控制,并且支持更多的高级功能。

16.2.2.1 例子:

16.2.2.1.1 公平性(Fairness)控制:

ReentrantLock提供了公平性控制的功能。通过在构造方法中指定fair参数为true,可以使得锁的获取按照线程请求的顺序进行,即先到先得的原则。这样可以避免某些线程一直获取不到锁而产生饥饿现象。

相比之下,synchronized关键字并没有提供公平性控制的选项,它的锁获取是非公平的。在多个线程同时竞争同一个锁时,synchronized无法保证等待时间最长的线程优先获得锁,可能会导致一些线程长时间等待锁而无法执行。

因此,如果对公平性有较高的要求,可以使用ReentrantLock来实现可重入锁,并通过设置fair参数为true来保证线程的公平竞争。

16.2.2.1.2 可中断性(Interruptibility):

假设有两个线程,线程A和线程B,它们竞争一个ReentrantLock锁。

import java.util.concurrent.locks.ReentrantLock;

public class InterruptExample {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            try {
                lock.lockInterruptibly(); // 可中断地获取锁
                System.out.println("线程A获得了锁");
                Thread.sleep(5000); // 模拟线程A执行一些操作
            } catch (InterruptedException e) {
                System.out.println("线程A被中断");
            } finally {
                if (lock.isHeldByCurrentThread()) {
                    lock.unlock();
                }
            }
        });

        Thread threadB = new Thread(() -> {
            try {
                Thread.sleep(2000); // 等待2秒
                threadA.interrupt(); // 中断线程A
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        threadA.start();
        threadB.start();
    }
}

线程A通过调用lock.lockInterruptibly()方法来可中断地获取锁。而线程B在等待2秒后,调用threadA.interrupt()方法来中断线程A。

如果线程A在等待锁的过程中被中断,会触发InterruptedException异常,然后线程A可以根据自己的需求进行相应的处理。在上述代码中,线程A会打印"线程A被中断"。

这个例子展示了ReentrantLock的可中断性,通过中断一个等待锁的线程,可以让它在等待过程中响应中断并进行相应的处理。而synchronized关键字并没有提供直接的中断支持,无法中断正在等待锁的线程。

 

16.2.2.1.3 条件变量(Condition)支持:

ReentrantLock内置了Condition接口,通过它可以创建多个条件变量,实现更灵活的线程协作。

Condition接口提供了以下几个方法:

  • await():使当前线程等待,并释放锁,直到被其他线程显式地唤醒或被中断。
  • awaitUninterruptibly():与await()类似,但不响应中断。
  • signal():唤醒一个等待该条件的线程。
  • signalAll():唤醒所有等待该条件的线程。

例子:

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionExample {
    private static ReentrantLock lock = new ReentrantLock();
    private static Condition condition = lock.newCondition();

    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            try {
                lock.lock();
                System.out.println("线程A开始等待");
                condition.await(); // 等待条件满足
                System.out.println("线程A被唤醒");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        Thread threadB = new Thread(() -> {
            try {
                Thread.sleep(2000); // 等待2秒
                lock.lock();
                System.out.println("线程B发出唤醒信号");
                condition.signal(); // 发出唤醒信号
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        });

        threadA.start();
        threadB.start();
    }
}

线程A通过调用condition.await()方法使自己等待条件满足,然后线程B在等待2秒后,通过调用condition.signal()方法唤醒线程A。

使用条件变量,我们可以实现更加灵活的线程协作,线程可以根据特定的条件进行等待或唤醒。这在一些生产者消费者模型、线程池等场景中非常有用。而synchronized关键字并没有直接提供这种条件变量的支持。

16.2.2.1.4 超时控制(Timeout):

,ReentrantLock提供了尝试获取锁的方法,并且可以指定一个超时时间。如果在指定时间内无法获取到锁,则可以继续执行其他操作,避免线程长时间等待。

ReentrantLock提供了以下几个尝试获取锁的方法:

  • tryLock():尝试获取锁,如果成功获取到锁则返回true,否则立即返回false
  • tryLock(long timeout, TimeUnit unit):尝试在指定的超时时间内获取锁,如果在指定时间内成功获取到锁则返回true,否则在超时后返回false

例子:

线程A调用lock.tryLock(3, TimeUnit.SECONDS)方法,在3秒内尝试获取锁。如果在3秒内成功获取到锁,则执行相应的操作,否则在超时后输出"线程A尝试获取锁超时"。

通过使用tryLock()方法并指定超时时间,我们可以避免线程长时间等待,并在超时后执行其他操作。这在一些需要控制等待时间的场景中非常有用。

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

public class TimeoutExample {
    private static ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) {
        Thread threadA = new Thread(() -> {
            try {
                if (lock.tryLock(3, TimeUnit.SECONDS)) { // 在3秒内尝试获取锁
                    try {
                        System.out.println("线程A获得了锁");
                        Thread.sleep(5000); // 模拟线程A执行一些操作
                    } finally {
                        lock.unlock();
                        System.out.println("线程A释放了锁");
                    }
                } else {
                    System.out.println("线程A尝试获取锁超时");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });

        Thread threadB = new Thread(() -> {
            try {
                Thread.sleep(2000); // 等待2秒
                lock.lock(); // 线程B获得锁
                System.out.println("线程B获得了锁");
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
                System.out.println("线程B释放了锁");
            }
        });

        threadA.start();
        threadB.start();
    }
}

tips:

TimeUnit.SECONDS 是 Java 中的一个枚举常量,它表示时间单位为秒。

lock.tryLock(3, TimeUnit.SECONDS) 表示在 3 秒内尝试获取锁。这里的 3 就是指定的时间,而 TimeUnit.SECONDS 则表示时间单位为秒。

  • TimeUnit.NANOSECONDS:纳秒
  • TimeUnit.MICROSECONDS:微秒
  • TimeUnit.MILLISECONDS:毫秒
  • TimeUnit.MINUTES:分钟
  • TimeUnit.HOURS:小时
  • TimeUnit.DAYS:天

 

你可能感兴趣的:(面试,职场和发展)