十五 . 面向对象的基本要素:封装,继承,多态
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
。然后,定义了两个子类 Cat
和 Dog
,它们分别继承自 Animal
并实现了 sound
方法。
在测试类中,创建了一个 Animal 类型的对象数组,并分别用 Cat 和 Dog 的实例来初始化数组的元素。然后通过循环遍历数组,并调用 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提供的可重入锁实现,它具有更细粒度的控制和更多的功能。相较于synchronized
,ReentrantLock
提供了以下优势:
- 公平性(Fairness)控制:ReentrantLock可以通过构造方法的参数来指定是否按照线程请求锁的顺序获取锁(公平性),以避免某些线程长时间等待锁而产生饥饿现象。
- 可中断性(Interruptibility):ReentrantLock提供了可中断的获取锁的方法,即线程在等待锁的过程中可以被其他线程中断,并通过捕获InterruptedException来处理中断事件。
- 条件变量(Condition)支持:ReentrantLock内置了Condition接口,可以创建多个条件变量,使线程能够在特定条件满足时等待或被唤醒,从而实现更灵活的线程协作。
- 超时控制(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
:天