面试题
面试题:面向对象设计7大原则
- 接口隔离:一个类对另一个类的依赖建立在最小的接口
- 依赖倒置:高层模块不能依赖底层模块
- 里氏替换:使用子类替换父类后,代码正常执行
- 开闭原则:对扩展开放,对修改关闭
- 迪米特原则:类于类之间尽量少的
- 单一职责:类的功能要单一
- 组合/聚合复用原则:尽量使用组合和聚合关系
单例模式有几种
单例模式主要分为:饿汉单例、懒汉单例
- 饿汉单例:创建时即创建对象,缺点对象存在时间过长,如果运行期间未使用该单例造成资源浪费
- 懒汉单例:延迟对象的创建到第一次使用,减少系统性能开销
饿汉单例不存在线程不安全的情况,懒汉存在线程不安全的情况
public class Singleton {
private static Singleton instance;
public static getInstance(){
if(instance == null){
synchronized(Singleton.class){
if(instance == null)instance = new Singleton();
}
}
return instance;
}
}
概述
设计模式主要有23种,这23种又主要分为创建型、结构型、行为型
创建型
工厂模式
作用
创建者和调用者分离,调用者可以不用关心对象如何创建
简单工厂
public class CarFactory {
public Car getCart(String carName){
if("宝马".equals(carName)){
return new BaomaCar();
}else if("奥迪".equals(carName)){
return new Aodi();
}
return null;
}
}
缺点
很明显,简单工厂不容易扩展,当想要在工厂中加一种车型时,只能修改原有的代码。
工厂方法
将工厂抽成接口,每个产品有不同的工厂,扩展时只需要在调用处修改即可
package com.sy.offer.week13;
/**
* @author 沈洋 邮箱:1845973183@qq.com
* @create 2022/1/21-14:12
**/
// 工厂接口
public interface CarFactory {
public Car getCar();
}
// 宝马汽车的工厂
class BaomaFactory implements CarFactory{
@Override
public Car getCar() {
return new Baoma();
}
}
// 奥迪汽车的工厂
class AodiFactory implements CarFactory{
@Override
public Car getCar() {
return new Aodi();
}
}
// 消费者
class Consumer{
public void test(){
Car car1 = new BaomaFactory().getCar();
Car car2 = new AodiFactory().getCar();
}
}
缺点
工厂方法解决了简单工厂扩展困难的问题, 在扩展时不需要修改原有的代码,但是需要定义的类变多了,结构更复杂
抽象工厂
抽象工厂是对工厂的抽象,抽象工厂不关注产品如何创建,只关注定义,由具体的工厂实现。
一般用于一个产品簇
缺点
抽象工厂如果发生变化,将修改大量的代码
单例设计模式
- 一个类在任何情况下都绝对只有一个实例
- 提供全局访问点
- 例如ServletContext、ServletContextConfig、ApplicationContext,数据库连接池。
饿汉式单例
类加载时,即初始化,并且创建单例对象。
适用于单例较少的情况
SpringIoC容器ApplicationContext就是典型的饿汉单例模式。
优点:
- 因为在线程还没出现时就创建了对象,所以是 线程安全的;
- 没有加锁,执行效率高
缺点:
- 运行期间一直占用内存,不使用时浪费内存。
实现饿汉单例
实现一:
public class HungrySingleton {
//先静态、后动态
//先属性,后方法
//先上后下
private static final HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
实现二:
public class HungrySingleton {
private static HungrySingleton hungrySingleton;
static {
hungrySingleton = new HungrySingleton();
}
private HungrySingleton(){};
public static HungrySingleton getInstance(){
return hungrySingleton;
}
}
懒汉式单例模式
-
不是线程安全的
-
被外部类调用时内部类才会加载
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton = null;
private LazySimpleSingleton() {
}
public static LazySimpleSingleton getInstance(){
if(lazySimpleSingleton==null) lazySimpleSingleton=new LazySimpleSingleton();
return lazySimpleSingleton;
}
}
普通锁
public class LazySimpleSingleton {
private static LazySimpleSingleton lazySimpleSingleton = null;
private LazySimpleSingleton() {
}
public synchronized static LazySimpleSingleton getInstance(){
if(lazySimpleSingleton==null) lazySimpleSingleton=new LazySimpleSingleton();
return lazySimpleSingleton;
}
}
双检锁
上述方法仍存在问题,加锁之后线程多了会对CPU造成压力,使用双重检查锁
不对整个类上锁,而只是针对getInstance()方法,增加了性能。
public class LazySimpleSingleton {
private volatile static LazySimpleSingleton lazySimpleSingleton = null;
private LazySimpleSingleton() {
}
public static LazySimpleSingleton getInstance(){
if(lazySimpleSingleton==null) {
synchronized (LazySimpleSingleton.class){
if(lazySimpleSingleton==null) lazySimpleSingleton=new LazySimpleSingleton();
}
}
return lazySimpleSingleton;
}
}
内部类
//这种形式兼顾饿汉式单例模式的内存浪费问题和synchronized的性能问题
//完美屏蔽了这两个缺点
public class LazyInnerClassSingleton {
//使用LazyInnerClassGeneral的时候,默认会先初始化内部类
//如果没使用,则内部类时不加载的
private LazyInnerClassSingleton() {
}
//static是为了使单例的空间共享,保证这个方法不会被覆写、重载
public static final LazyInnerClassSingleton getInstance(){
//在返回结果以前,一定会先加载内部类
return LazyHolder.LAZY;
}
//默认不加载
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
注册式单例设计模式
实现一、枚举式
- 将 每一个实例都登记到一个地方,以唯一标识符获取
- 分两种模式:①枚举式单例 ②容器里式单例
枚举单例模式,由于枚举在JDK语法的特殊性及反射为枚举做的保护。使得枚举单例做为推荐的单例实现方法。
public enum EnumSingleton {
INSTANCE;
private Object data;
private Object getData() {
return data;
}
public void setData(Object data){
this.data = data;
}
public static EnumSingleton getInstance(){
return INSTANCE;
}
}
实现二、容器式单例
- 使用广泛,但非线程安全。
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
public class ContainerSingleton {
private ContainerSingleton(){}
private static Map<String,Object> ioc = new ConcurrentHashMap<String,Object>();
private static Object getBean(String className){
synchronized (ioc){
if(!ioc.containsKey(className)){
Object obj = null;
try {
obj = Class.forName(className).newInstance();
ioc.put(className,obj);
} catch (Exception e) {
e.printStackTrace();
}
return obj;
}else {
return ioc.get(className);
}
}
}
}
破坏单例模式
反射破坏单例模式
- 通过反射机制可以得到一个新的实例,这样就破坏了单例模式的概念。
测试问题:
package com.sy.day2.t5;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
public class LazyInnerClassSingletonTest {
public static void main(String[] args) {
try {
Class<?> clazz = LazyInnerClassSingleton.class;
//通过反射获取私有的构造方法
Constructor c = clazz.getDeclaredConstructor(null);
//强制访问
c.setAccessible(true);
//暴力初始化
Object o1 = c.newInstance();
//调用了两次构造方法,相当于“new”了两次
Object o2 = c.newInstance();
System.out.println(o1==o2);
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (InstantiationException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
解决:在构造方法中加入逻辑判断,单例对象为空时抛出异常
package com.sy.day2.t5;
//这种形式兼顾饿汉式单例模式的内存浪费问题和synchronized的性能问题
//完美屏蔽了这两个缺点
public class LazyInnerClassSingleton {
//使用LazyInnerClassGeneral的时候,默认会先初始化内部类
//如果没使用,则内部类时不加载的
private LazyInnerClassSingleton() {
if(LazyHolder.LAZY!=null){
throw new RuntimeException("不允许创建多个实例");
}
}
//static是为了使单例的空间共享,保证这个方法不会被覆写、重载
public static final LazyInnerClassSingleton getInstance(){
//在返回结果以前,一定会先加载内部类
return LazyHolder.LAZY;
}
//默认不加载
private static class LazyHolder{
private static final LazyInnerClassSingleton LAZY = new LazyInnerClassSingleton();
}
}
反序列化破坏单例模式
问题:对象序列化存入磁盘后,下次进行反序列化时,如果不加限制反序列化所得到的对象与当前对象并不相同,违背了单例设计模式。
测试:
package com.sy.day2.t5;
import java.io.Serializable;
public class SeriableSingleton implements Serializable {
//序列化就是把内存中的状态通过转换成字节码的形式
//从而转换一个I/O流,写入其他地方(可以是磁盘,网络I/O)
//内存中的状态持久化
//反序列化就是将持久化的字节码内容转换成I/O流
//通过I/O流的读取,进而将读取的内容转换为Java对象
//转换过程中会重新创建对象(相当于重新new一个对象)
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton() {}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
}
package com.sy.day2.t5;
import java.io.*;
public class SeriableSingletonTest {
public static void main(String[] args) {
SeriableSingleton s1 = null;
SeriableSingleton s2 = SeriableSingleton.getInstance();
FileOutputStream fos = null;
try {
fos = new FileOutputStream("SeriableSingleton.obj");
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s2);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SeriableSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
s1 = (SeriableSingleton) ois.readObject();
ois.close();
System.out.println(s1);
System.out.println(s2);
System.out.println(s1==s2);
} catch (Exception e) {
e.printStackTrace();
}
}
}
解决:实现Serializable接口,增加readResolve方法
package com.sy.day2.t5;
import java.io.Serializable;
public class SeriableSingleton implements Serializable {
//序列化就是把内存中的状态通过转换成字节码的形式
//从而转换一个I/O流,写入其他地方(可以是磁盘,网络I/O)
//内存中的状态持久化
//反序列化就是将持久化的字节码内容转换成I/O流
//通过I/O流的读取,进而将读取的内容转换为Java对象
//转换过程中会重新创建对象
public final static SeriableSingleton INSTANCE = new SeriableSingleton();
private SeriableSingleton() {}
public static SeriableSingleton getInstance(){
return INSTANCE;
}
private Object readResolve(){
return INSTANCE;
}
}
注:虽然解决了单例设计模式被破坏的问题,但实际上是实例化了两次,只是readResolve方法覆盖了第一次的对象。
建造者模式
将复杂的对象创建步骤分散,按一定步骤调用方法将对象创建出来lombok的Builder注解就是典型的建造者模式
使用静态内部类来实现
public class User {
private String name;
private Integer age;
public User(String name, Integer age) {
this.name = name;
this.age = age;
}
public static User.UserBuilder builder() {
return new User.UserBuilder();
}
public String getName() {
return this.name;
}
public Integer getAge() {
return this.age;
}
public String toString() {
return "User(name=" + this.getName() + ", age=" + this.getAge() + ")";
}
public static class UserBuilder {
private String name;
private Integer age;
UserBuilder() {
}
public User.UserBuilder name(String name) {
this.name = name;
return this;
}
public User.UserBuilder age(Integer age) {
this.age = age;
return this;
}
public User build() {
return new User(this.name, this.age);
}
public String toString() {
return "User.UserBuilder(name=" + this.name + ", age=" + this.age + ")";
}
}
}
原型模式
以一个对象为模板,创建新的对象,使用clone方法等实现
使用clone方法必须要实现Cloneable接口,否者在调用时会报错
并且这里涉及到浅拷贝和深拷贝的问题
结构型模式
适配器模式
将一个类的接口转换成客户希望的另一个接口,Adapter模式使得原本不兼容的接口可以一起工作
适配器模式主要分三个类:待转换的类、适配器类、目标类
适配器的目的就是将待转换的类转换成目标类
行为型
模板模式
提供一个抽象模板,方法的调用在父类中进行,子类实现小的方法。由父类关键方法调用
public abstract class Game {
abstract void initialize();
abstract void startPlay();
abstract void endPlay();
//模板
public final void play(){
//初始化游戏
initialize();
//开始游戏
startPlay();
//结束游戏
endPlay();
}
}
public class Football extends Game {
@Override
void endPlay() {
System.out.println("Football Game Finished!");
}
@Override
void initialize() {
System.out.println("Football Game Initialized! Start playing.");
}
@Override
void startPlay() {
System.out.println("Football Game Started. Enjoy the game!");
}
}
策略模式
一个类的行为或其算法可以在运行时更改
// 策略接口
public interface Strategy {
public int doOperation(int num1, int num2);
}
// 定义策略一
class OperationAdd implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 + num2;
}
}
// 定义策略二
class OperationSubtract implements Strategy{
@Override
public int doOperation(int num1, int num2) {
return num1 - num2;
}
}
// 实际调用策略的类
class Context {
private Strategy strategy;
public Context(Strategy strategy){
this.strategy = strategy;
}
// 动态改变strategy即可实现动态修改策略
public int executeStrategy(int num1, int num2){
return strategy.doOperation(num1, num2);
}
}