1. OOA、OOD、OOP、OOPL的涵义。
-
OOA(全称:
Object Oriented Analysis
):面向对象分析。 -
OOD(全称:
Object Oriented Design
):面向对象设计。 -
OOP(全称:
Object Oriented Programming
):面向对象编程。 -
OOPL(全称:
Object Oriented Programming Language
):面向对象编程语言。
2.面向对象的七大设计原则。
这 7 种设计原则是软件设计模式必须尽量遵循的原则,各种原则要求的侧重点不同。其中,【开闭原
则】是总纲,它告诉我们要【对扩展开放,对修改关闭】;【⾥⽒替换原则】告诉我们【不要破坏继承
体系】;【依赖倒置原则】告诉我们要【⾯向接⼝编程】;【单⼀职责原则】告诉我们实现【类】要
【职责单⼀】,也就是如何定义⼀个类,如何去实现类的封装;【接⼝隔离原则】告诉我们在设计【接
⼝】的时候要【精简单⼀】;【迪⽶特法则】告诉我们要【降低耦合度】;【合成复⽤原则】告诉我们
要【优先使⽤组合或者聚合关系复⽤,少⽤继承关系复⽤】。
设计原则名称 | 定义 | 使用频率 |
---|---|---|
单⼀职责原则(Single Responsibility Principle, SRP) | ⼀个类只负责⼀个功能领域中的相应职责 | ★★★★☆ |
开闭原则(Open-Closed Principle, OCP) | 软件实体应对扩展开放,⽽对修改关闭 | ★★★★★ |
⾥⽒代换原则(Liskov Substitution Principle, LSP) | 所有引⽤基类对象的地⽅能够透明地使⽤其⼦类的对象 | ★★★★★ |
依赖倒转原则(Dependence Inversion Principle, DIP) | 抽象不应该依赖于细节,细节应该依赖于抽象 | ★★★★★ |
接⼝隔离原则(Interface Segregation Principle, ISP) | 使⽤多个专⻔的接⼝,⽽不使⽤单⼀的 总接⼝ | ★★☆☆☆ |
合成复⽤原则(Composite Reuse Principle, CRP) | 尽量使⽤对象组合,⽽不是继承来达到复⽤的⽬的 | ★★★★☆ |
迪⽶特法则(Law of Demeter, LoD) | ⼀个软件实体应当尽可能少地与其他实体发⽣相互作⽤ | ★★★☆☆ |
3.23种设计模式。
什么是设计模式?
-
设计模式(Design pattern)代表了最佳的实践,通常被有经验的⾯向对象的软件开发⼈员所采⽤。
-
设计模式是软件开发⼈员在软件开发过程中⾯临的⼀般问题的解决⽅案。这些解决⽅案是众多软件开发⼈员经过相当⻓的⼀段时间的试验和错误总结出来的。
-
设计模式是⼀套被反复使⽤、多数⼈知晓的、经过分类编⽬的、代码设计经验的总结。使⽤设计模式是为了可重⽤代码、让代码更容易被他⼈理解、保证代码可靠性。
-
设计模式不是⼀种⽅法和技术,⽽是⼀种思想。
-
设计模式和具体的语⾔⽆关,学习设计模式就是要建⽴⾯向对象的思想,尽可能的⾯向接⼝编程,低耦合,⾼内聚,使设计的程序可复⽤。
-
学习设计模式能够促进对⾯向对象思想的理解,反之亦然。它们相辅相成。
设计模式的分类
总体来说,设计模式按照功能分为三类23种:
-
创建型(5种) : ⼯⼚模式、抽象⼯⼚模式、单例模式(重点)、原型模式、构建者模式 。
-
结构型(7种): 适配器模式、装饰模式、代理模式(重点) 、外观模式、桥接模式、组合模式、享元模式
-
⾏为型(11种): 模板⽅法模式、策略模式 、观察者模式、中介者模式、状态模式、 责任链模式 、命令模式、迭代器模式、访问者模式、解释器模式、备忘录模式。
4.单例模式中的三种保证线程安全的写法。
双重检查锁(DCL)
public class DoubleCheckLockSingleton {
private static volatile DoubleCheckLockSingleton instance;
private DoubleCheckLockSingleton() {
if(instance != null){
// gun
}
public static DoubleCheckLockSingleton getInstance() {
if (instance == null) {
synchronized (DoubleCheckLockSingleton.class) {
if (instance == null) {
instance = new DoubleCheckLockSingleton();
}
}
}
return instance;
}
public void tellEveryone() {
System.out.println("This is a DoubleCheckLockSingleton " +this.hashCode());
}
private Object readResolve() {
}
}
注意:
volatile关键字在此处起了什么作⽤?
为何要执⾏两次 instance == null 判断?
静态内部类
public class StaticInnerHolderSingleton {
// 静态内部类
private static class SingletonHolder {
private static final StaticInnerHolderSingleton INSTANCE =
new StaticInnerHolderSingleton();
}
private StaticInnerHolderSingleton() {}
public static StaticInnerHolderSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
public void tellEveryone() {
System.out.println("This is a StaticInnerHolderSingleton" +this.hashCode());
}
}
这种⽅式是通过什么机制保证线程安全性与延迟加载的?(注意,这是Java单例的两⼤要点,必须
保证)
枚举
public enum EnumSingleton {
INSTANCE;
public void tellEveryone() {
System.out.println("This is an EnumSingleton " + this.hashCode());
}
}
Java枚举的本质是什么?
这种⽅式⼜是通过什么机制保证线程安全性与延迟加载的?
5.写出两种攻击单例模式的方式的名称(如何破坏一个单例?)
反射攻击
public class SingletonAttack {
public static void main(String[] args) throws Exception {
reflectionAttack();
}
public static void reflectionAttack() throws Exception {
//通过反射,获取单例类的私有构造器
Constructor constructor =
DoubleCheckLockSingleton.class.getDeclaredConstructor();
//设置私有成员的暴⼒破解
constructor.setAccessible(true);
// 通过反射去创建单例类的多个不同的实例
DoubleCheckLockSingleton s1 = (DoubleCheckLockSingleton)constructor.newInstance();
// 通过反射去创建单例类的多个不同的实例
DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)constructor.newInstance();
s1.tellEveryone();
s2.tellEveryone();
System.out.println(s1 == s2);
}
}
执⾏结果如下:
This is a DoubleCheckLockSingleton 1368884364
This is a DoubleCheckLockSingleton 401625763
false
这种⽅法⾮常简单暴⼒,通过反射侵⼊单例类的私有构造⽅法并强制执⾏,使之产⽣多个不同的实例,
这样单例就被破坏了。
序列化攻击
这种攻击⽅式只对实现了Serializable接⼝的单例有效,但偏偏有些单例就是必须序列化的。现在假设
DoubleCheckLockSingleton类已经实现了该接⼝,上代码:
public class SingletonAttack {
public static void main(String[] args) throws Exception {
serializationAttack();
}
public static void serializationAttack() throws Exception {
// 对象序列化流去对对象进⾏操作
ObjectOutputStream outputStream = new ObjectOutputStream(new
FileOutputStream("serFile"));
//通过单例代码获取⼀个对象
DoubleCheckLockSingleton s1 = DoubleCheckLockSingleton.getInstance();
//将单例对象,通过序列化流,序列化到⽂件中
outputStream.writeObject(s1);
// 通过序列化流,将⽂件中序列化的对象信息读取到内存中
ObjectInputStream inputStream = new ObjectInputStream(new
FileInputStream(new File("serFile")));
//通过序列化流,去创建对象
DoubleCheckLockSingleton s2 = (DoubleCheckLockSingleton)inputStream.readObject();
s1.tellEveryone();
s2.tellEveryone();
System.out.println(s1 == s2);
}
}
执⾏结果如下:
This is a DoubleCheckLockSingleton 777874839
This is a DoubleCheckLockSingleton 254413710
false
为什么会发⽣这种事?⻓话短说,在 ObjectInputStream.readObject() ⽅法执⾏时,其内部⽅法 readOrdinaryObject ()中有这样⼀句话:
//其中desc是类描述符
obj = desc.isInstantiable() ? desc.newInstance() : null;
也就是说,如果⼀个实现了Serializable/Externalizable接⼝的类可以在运⾏时实例化,那么就调⽤newInstance()⽅法,使⽤其默认构造⽅法反射创建新的对象实例,⾃然也就破坏了单例性。要防御序列化攻击,就得将instance声明为transient,并且在单例中加⼊以下语句:
private Object readResolve() {
return instance;
}
这是因为在上述readOrdinaryObject()⽅法中,会通过卫语句 desc.hasReadResolveMethod() 检查类
中是否存在名为readResolve()的⽅法,如果有,就执⾏ desc.invokeReadResolve(obj) 调⽤该⽅
法。readResolve()会⽤⾃定义的反序列化逻辑覆盖默认实现,因此强制它返回instance本身,就可以防
⽌产⽣新的实例。
6.volatile可以解决的问题
-
防止重排序
-
实现可见性
-
保证原子性