一、对象之间的关系
1. 综述
在Java中对象与对象的关系总体分为四类,分别是:依赖、关联、聚合和组合。
依赖关系:是类和类之间的联接。依赖关系表示一个类依赖于另外一个类的定义,一般而言,依赖关系在Java语言中体现为局域变量、方法的形参,或者对静态方法的调用。
关联关系:是类和类之间的联接,他使一个类知道另外一个类的属性和方法。关联可以是双向的,也可以是单向的。在Java语言中,关联关系一般使用成员变量来实现。
聚合关系:是关联关系的一种,是强的关联关系。聚合是整体和个体之间的关系。
组合关系:是关联关系的一种,是比聚合关系强的关系。
2. 依赖关系
个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是B类的变化会影响到A。
class A{
public void fun_A(){
...
}
}
class B{
public void fun1(A a){//使用参数方式发生与A的依赖关系,fun1方法依赖于A
a.fun_A();
}
public void fun2(){//使用创建对象的方式与A发生依赖关系,fun2方法依赖于A
A a = new A();
a.fun_A();
}
}
3. 关联关系
表现在代码上,就是一个类包含另一个类的实例,通常表现为被关联类以类属性的形式出现在关联类的类定义中,也可以表现为关联类引用了一个类型为被关联类的全局变量。关联可以使单向的,也可以使双向的。
依赖和关联的区别在于依赖是使用,关联是拥有。
class A{
public void fun_A(){
}
}
class B{
private A a ;
public B(A a){
this.a =a ;
}
public void work(){
a.fun_A() ;
}
}
4. 聚合关系
它是一种强关联关系(has-a),聚合关系是整体和个体/部分之间的关系,一种单项关系。
与关联关系的区别:关联关系的两个类处于同一个层次上,而聚合关系的两个类处于不同的层次上,一个是整体,一个是个体/部分
class A{
}
class B{
private A a ;//与上面关联的代码不同之处在于使用A对象构造B,也就是说B是A的不可缺少的一部分
public B(A a){
this.a =a ;
}
}
5. 组合关系
组合也是关联关系的一种(is-a),但它是比聚合关系更强的关系,也是整体/部分之间的关系,但是生命周期相同。
聚合和组合关系的区别:
1、依赖性区别
聚合中的两种类(或实体)是可以单独存在的,不会相互影响;被关联的一方可以独立于关联一方,依赖性不强。
相反,组合中的两个实体(或者类)是高度依赖于彼此的,它们之间会相互影响。
2、生命周期的不同
在聚合关系中的两个类(或实体)的生命周期是不同步;
但在组合关系中的两个类(或实体)的生命周期是同步的。
3、关联强度的不同
聚合关联程度(耦合性)比组合要弱;
在各种对象关系中,关系的强弱顺序为:泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖。
4、关系类型的区别
聚合代表了has-a关系,一种单向关系;组合代表了part-of关系。
class A{
...
}
class B{
private A a;
public B(){
a= newA() ; //强调了生命周期相同
}
}
二、七大原则
1. 单一职责原则
在软件系统中,一个类只负责一个功能领域中的相应职责
好处:
- 类的复杂性降低,实现什么职责都有清晰明确的定义
- 可读性提高,复杂性降低,那当然可读性提高了
- 可维护性提高,那当然了,可读性提高,那当然更容易维护了
- 变更引起的风险降低,如果接口的单一职责做得好,一个接口修改只对相应的实现类有影响,对其他的接口无影响,这对系统的扩展性、维护性都有非常大帮助
注意:
单一职责的应用应该要有一个合理的限度,并不是越单一越好。如果类的功能越单一,那么就会产生越多的类,类的数量越多就会造成整个系统的结构越复杂。所以我们在设计系统的时候需要找到一个合理的平衡点。
2. 开放封闭原则
一个软件实体应当对扩展开放,对修改关闭。在设计一个模块的时候,应当使这个模块可以在不被修改的前提下被扩展,即实现在不修改源代码的情况下改变这个模块的行为。
好处:
- 对扩展开放,意味着有新的需求或变化时,可以对现有代码进行扩展,以适应新的情况。
- 对修改封闭,意味着类一旦设计完成,就可以独立完成其工作,而不要对其进行任何的修改
样例
一个计算器的例子:
//计算器类,可以根据输入的参数和操作进行一定运算
public class Cal{
public double oper( String oper , double num1, double num2){
double rst = 0;
if(oper.equals("+")){
Add add = new Add();
rst = add.cal(num1,num2) ;
}else if(oper.equals("-")){
sub sub = new Sub();
rst = sub.cal( num1, num2);
}
return rst;
}
}
使用开闭原则对其进行修改:
//定义一个计算接口
public interface Cal {
public double cal(double num1,double num2);
}
//加法实现类
public class Add implements Cal {
@Override
public double cal(double num1, double num2) {
return num1+num2;
}
}
//减法实现类
public class Sub implements Cal {
@Override
public double cal(double num1, double num2) {
return num1-num2;
}
}
//工厂类
public class Factory {
public static Cal createInstance(String oper){
Cal cal = null;
switch (oper){
case "+":
cal = new Add();
break;
case "-":
cal = new Sub();
break;
}
return cal;
}
}
public class MainTest {
public static void main(String[] args) {
//收集数据
Scanner input = new Scanner(System.in);
System.out.println("输入num1");
double num1 = input.nextDouble();
System.out.println("输入运算符");
String oper = input.next();
System.out.println("输入num2");
double num2 = input.nextDouble();
//调用工厂类的方法获取具体的计算实例
Cal cal = Factory.createInstance(oper);
double result = cal.cal(num1, num2);
//输出结果
System.out.println(result);
}
}
分析:原本的代码,如果后期要添加新的操作(比如乘/除)就需要在原来的代码上进行修改,显然这违背了开放封闭原则。修改的思想是,使用一个计算的接口,每一种操作都实现计算器接口的一项功能,在工厂类中调用该计算器接口,根据不同的操作符生成不同的操作对象。如果要扩展操作功能,就可以扩展Cal接口。此处,其实Factory也不符合开闭原则,可以使用反射对其进行修改:
//工厂类
public class Factory {
public static Cal createInstance(String oper) {
Cal cal = null;
try {
//加载配置文件
Properties properties = new Properties();
properties.load(Factory.class.getClassLoader().getResourceAsStream("oper.properties"));
//获取需要执行类的全类名
String clz = properties.getProperty(oper);
System.out.println(clz);
//动态加载
Class clazz = Class.forName(clz);
//创建实例
cal = (Cal)clazz.newInstance();
} catch (Exception e) {
e.printStackTrace();
}
return cal;
}
}
可以使用配置文件的方式对Factory的功能进行扩充:
//oper.properties配置文件:
+=com.zsn.test.Add
-=com.zsn.test.sub
这样经过上述的修改,如果想要扩充计算方法,只需要在配置文件中添加相应运算符指定新的运算类即可。以上也被称之为简单工厂模式(后文详细讲解)。
3. 依赖倒置原则
高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
好处: 当两个模块之间存在紧密的耦合关系时,可以在依赖之间定义一个抽象的接口使得高层模块调用接口,而底层模块实现接口的定义,以此来有效控制耦合关系。这样,高层模块就不再依赖于底层模块。
和开放封闭原则的关系:依赖倒置类似于实现开放封闭原则的方法。
样例:
//定义奔驰小车类
class Benz {
public void run(){
System.out.println("大奔来了,前面的让一让……");
}
}
//定义一个司机类(能开奔驰小车)
class Driverc {
public void drive(Benz benz){
System.out.println("老司机开车了,上车的赶快……");
benz.run();
}
}
public class Client {
public static void main( String[] args){
Benz benz = new Benz( );
Driverc driver = new DriverC();
//让司机去开奔驰车
driver.drive(benz) ;
}
}
优化修改之后:
//定义小车接口
interface Car {
public void run();
}
//定义奔驰小车类(实现car接口)
class Benz implements Car{
@override
public void run(){
System.out.print1n("大奔来了,前面的让一让……");
}
}
//BMW类,实现car接口
class BMw implements Car i
@override
public void run() {
System.out.println("BMw来了,前面的让一让……");
}
}
//定义一个司机类(能开小车)
class Driverc {
public void drive(car car){
System.out.println("老司机开车了,上车的赶快……");
car.run();
}
public class client {
public static void main(String[] args) i
Car car1 = new Benz();
Car car2 = new BMIW();
Driverc driver = new DriverC();
//让司机去开奔驰车
driver.drive(car1);
//让司机去开BMW
driver.drive(car2);
}
}
分析:原来的程序,司机直接依赖于具体的实现类(大奔),这样如果该司机要开新的车(BMw),就需要对司机的drive函数进行修改,这违背了开放封闭原则。优化的程序中,司机直接调用接口(车)中的方法,而不同类型的车直接实现车接口,就可以做到扩展功能而不修改代码(开闭原则)。
4. 里氏替换原则
子类必须可以代替父类。基于该原则可知:在继承时,子类尽量不要重写父类的方法,子类可以扩展新的方法。
好处: 能保证继承复用是可靠的,父类替换子类不会产生任何问题。
经典问题:
“鸵鸟非鸟”:鸟是基类,鸵鸟是鸟的实现类,该类需要覆盖原来的fly()
方法,这样鸵鸟就不能代替鸟。
“长方形不是正方形”:长方形是基类,正方形是子类,正方形计算面积的area()
方法需要覆盖基类的方法,这样使用正方形代替长方形是就会出现一些问题。
样例:
class A {
public void fun(int a, int b) {
System.out.println(a + "+" + b + "=" + (a + b));
}
}
class B extends A {
@Override
public void fun(int a, int b) {
System.out.println(a + "-" + b + "=" + (a - b));
}
}
public class demo {
public static void main(String[] args) {
System.out.println("父类的运行结果");
A a = new A();
a.fun(1, 2);
//父类存在的地方,可以用子类替代
//子类B替代父类A
System.out.println("子类替代父类后的运行结果");
B b = new B();
b.fun(1, 2);
}
}
分析:可以看到,子类的方法如果覆盖的父类的方法,则在使用子类代替父类时结果是不一样的。
5. 接口隔离原则
使用多个小的专门的接口,而不要使用一个大的总接口。
分析:个类对另外一个类的依赖应该建立在最小的接口上,胖接口会导致实现的类型必须完全实现接口的所有方法,显然这是一种浪费。可以通过接口的多继承来实现客户的需求。
样例:
/**
* 定义接口1,里面定义了N个方法
*/
interface Interface1{
void fun1();
void fun2();
void fun3();
void fun4();
void fun5();
}
/**
* Class1类中具有多个无用的方法。
* 虽然在这里可以不用去写太多的代码,但是在调用的时候会引起混乱
*/
public class Class1 implements Interface1{
@Override
public void fun1() {
system.out.println("这个方法是有用的.....");
}
@Override
public void fun2() {
System.out.println("这个方法是有用的.....");
}
@Override
public void fun3() {
}
@Override
public void fun4() {
}
@Override
public void fun5() {
}
}
使用接口隔离优化的代码:
/**
* 首先将胖接口根据具体的需求隔离成多个小的接口
*/
interface Interface1{
void fun1();
void fun2();
}
interface Interface2{
void fun3();
void fun4();
}
public interface Interface3{
void fun5();
}
//如果一个类需要实现方法fun1,fun2,fun5。再定义一个接口,继承接口1和接口3
//这时,接口4利用接口的多继承特性,就具有了三个方法的定义
interface Interface4 extends Interface1, Interface3{
}
//实现类实现接口4
public class Class1 implements Interface4{
@Override
public void fun1() {
}
@Override
public void fun2() {
}
@Override
public void fun5() {
}
}
分析:使用一个大的接口,如果创建实现类的话需要实现所有的方法。而使用小接口,多接口继承的方式,则避免了不必要的方法实现。
6. 合成复用原则
尽量使用对象组合,而不是继承来达到复用的目的。也就是,不要为了继承而继承。
好处:
- 使用继承的方式来进行代码复用,缺点在于继承来的方法相当于是自身的方法,那么这个方法如果在需求发生变更的时候,就无法改变(必须要修改源代码来达到目的)。这破坏开放封闭原则。
- 而使用合成(组合)复用,方法是其他类中的方法,在本类中只是去调用其他的类。所以可以使用依赖倒置原则,可以达到在需求发生变更的时候,扩展现有代码达到目的。
- 组合/聚合可以使系统更加灵活,类与类之间的耦合度降低,一个类的变化对其他类造成的影响相对较少,因此一般首选使用组合/聚合来实现复用
样例:
class DBConnection {
public String getConnection() {
return "MySQL数据库连接";
}
}
class UserDao extends DBConnection {
public void addUser(){
//此处复用了父类的方法
String connection = super.getConnection();//省略后续添加客户的操作
}
}
public class Test {
public static void main(String[] args){
UserDao userDao = new UserDao();
userDao.addUser();
}
}
优化后的代码:
abstract class DBConnection {
public abstract String getConnection();
}
class MySQLConnection extends DBConnection {
@Override
public String getConnection() {
return "MySQL数据库连接";
}
class OracleConnection extends DBConnection {
@Override
public String getConnection() {
return "Oracle数据库连接";
}
class UserDao {
private DBConnection connection;
//通过组合(聚合)的方式调用外部对象的方法,外部方法可以使用依赖倒置进行扩展
public void setConnection(DBConnection connection) {
this.connection = connection;
}
public void addUser() {
//省略后续利用connection添加客户的操作
}
}
public class Test {
public static void main(String[] args) {
UserDao userDao = new UserDao();
userDao.setConnection(new MySQLConnection());
userDao.addUser();
userDao.setConnection(new OracleConnection());
userDao.addUser();
}
}
分析:如果使用继承的方式复用父类中的代码,则需要扩展时,就需要修改父类代码,违背开放封闭原则。优化后,使用组合的方法,相当于调用外部对象的方法,该对象可以通过依赖倒置的原则进行方法扩展。
7. 迪米特法则(最少知识原则)
又称为最少知识原则(Least Knowledge Principle,LKP),含义为:不要和“陌生人”说话;只与你的直接朋友通信。
好处:尽可能少的与其他实体发生相互作用(与其他实体发生关系越多,那么耦合度就越高),降低耦合。
样例:
//商品类
class Goods{
}
//员工类
class Staff {
public void checkNumber(List<Goods> goods) {
System.out.println("目前超市内商品总数为: "+goods.size());
}
}
//老板类
class Boss {
public void requireCheckNumber(Staff staff) {
List<Goods> goodsList = new ArrayList<Goods>();
for (int i = 0; i<50; i++) {
goodsList.add(new Goods());
}
staff.checkNumber(goodsList);
}
}
//测试类
public class Test{
public static void main(String[] args){
Boss boss = new Boss();
Staff staff = new Staff();
boss.requireCheckNumber(staff);
}
}
代码优化:
class Boss {
public void requireCheckNumber(Staff staff) {
staff.checkNumber();
}
}
public class Staff {
public void checkNumber() {
List<Goods> goodsList = new ArrayList<Goods>();
for ( int i = 0; i < 50; i++) {
goodsList.add(new Goods());
}
System.out.println("目前超市内商品总数为: "+goodsList.size());
}
}
分析:未优化前,老板类中既出现了商品类,又出现了员工类,不符合迪米特法则。优化后,商品类的信息通过员工类传递给了老板类。
8. 七大原则总结
单一职责原则:要求在软件系统中,一个类只负责一个功能领域中的相应职责。
开闭原则要求:一个软件实体应当对扩展开放,对修改关闭,即在不修改源代码的基础上扩展一个系统的行为。
里氏代换原则:可以通俗表述为在软件中如果能够使用基类对象,那么一定能够使用其子类对象。
依赖倒转原则:要求抽象不应该依赖于细节,细节应该依赖于抽象;要针对接口编程,不要针对实现编程。
接口隔离原则:要求客户端不应该依赖那些它不需要的接口,即将一些大的接口细化成一些小的接口供客户端使用。
合成复用原则:要求复用时尽量使用对象组合,而不使用继承。
迪米特法则:要求一个软件实体应当尽可能少的与其他实体发生相互作用。
三、设计模式
1. 单例模式
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
注意:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
关键代码:构造函数是私有的。
单例模式有以下几种实现方式:
1.1 懒汉模式
描述: 这种方式是最基本的实现方式,这种实现最大的问题就是不支持多线程。因为没有加锁 synchronized,所以严格意义上它并不算单例模式。
缺点: 不要求线程安全,在多线程不能正常工作。
public class Singleton {
private static Singleton instance;
private Singleton(){};
static Singleton getInstance(){
if(instance == null){//懒汉模式:要调用时没有才创建
instance = new Singleton();
}
return instance;
}
}
class Test{
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.hashCode());
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton2.hashCode());
/*
结果为:
1846274136
1846274136
*/
}
}
1.2 饿汉模式
描述:这种方式比较常用,但容易产生垃圾对象。
优点:线程安全,没有加锁,执行效率会提高。
缺点:类加载时就初始化,浪费内存。
public class Singleton {
//类加载的时候就创建对象
static private Singleton instance = new Singleton();
private Singleton(){}
static Singleton getInstance(){
return instance;
}
}
class Test{
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
System.out.println(singleton.hashCode());
Singleton singleton2 = Singleton.getInstance();
System.out.println(singleton2.hashCode());
}
}
1.3 线程安全懒汉模式
使用 synchronized
关键字对getInstance()
方法进行修饰
优点:线程安全,实现简单
缺点:效率低下
public class Singleton {
private static Singleton instance;
private Singleton(){}
public static synchronized Singleton getInstance(){
if(instance == null){
instance = new Singleton();
}
return instance;
}
}
1.4 双重校验锁(DCL,即 double-checked locking)
优点:使用synchronized代码块,效率高,且线程安全
缺点:写起来需要注意volatile关键字修饰单例对象以及双重校验单例是否为空。
public class Singleton {
private static volatile Singleton instance = null;
private Singleton(){}
public static Singleton getInstance(){
if(instance == null){
synchronized (Singleton.class){
if(instance == null){
instance = new Singleton();
}
}
}
return instance;
}
}
1.5 使用静态内部类
在Singleton类中定义一个静态内部类,静态内部类中包含一个Singleton的实例成员变量。相比于1.1的懒汉模式,静态内部类的初始化发生在静态方法/静态变量调用的时候,于是就不再有资源的浪费。
优点:线程安全,简单
public class Singleton {
private static class InnerClass{
static final Singleton INSTANCE = new Singleton();
}
private Singleton(){}
public static Singleton getInstance(){
return InnerClass.INSTANCE;//在调用静态成员变量时该变量才会初始化创建一个Singleton实例,且该实例不可变唯一。
}
}
1.6 使用枚举
描述: 这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。
这种方式是 Effective Java 作者 Josh Bloch提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。
public enum Singleton {
INSTANCE;
public void func(){
System.out.println("执行了func方法");
}
}
class Test{
public static void main(String[] args) {
Singleton singleton1 = Singleton.INSTANCE;
System.out.println(singleton1.hashCode());
Singleton singleton2 = Singleton.INSTANCE;
System.out.println(singleton2.hashCode());
}
}
1.7 总结
经验之谈: 一般情况下,不建议使用第 1 种和第 3 种懒汉方式,建议使用第 2 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种登记方式。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。
2.工厂模式
核心本质:创建对象不使用new,而是交给工厂来完成。将选择实现类、创建对象统一管理和控制,从而将调用者和实现类解耦。
不使用工厂模式的代码:
接口:
public interface Car {
void name();
}
Byd类:
public class Byd implements Car {
@Override
public void name() {
System.out.println("比亚迪");
}
}
Tesla类:
public class Tesla implements Car {
@Override
public void name() {
System.out.println("特斯拉");
}
}
测试类:
public class Test {
public static void main(String[] args) {
Car car1 = new Byd();
Car car2 = new Tesla();
car1.name();
car2.name();
}
}
2.1 简单工厂模式
使用简单工厂模式对上述代码进行修改:
工厂类:
public class CarFactory {
public static Car getCar(String string){
if(string.equals("比亚迪")){
return new Byd();
}else if(string.equals("特斯拉")){
return new Tesla();
}else return null;
}
}
测试类:
public class Test {
public static void main(String[] args) {
Car car1 = CarFactory.getCar("比亚迪");
Car car2 = CarFactory.getCar("特斯拉");
car1.name();
car2.name();
}
}
依赖图
优势: 不需要客户自己创建对象,将创建对象的任务交给工厂来完成。如果要扩展其他类型的车,则可以直接添加一个车类实现车接口,然后在工厂方法中创建具体的车对象。
缺点: 新增其他类型的车,仍然需要修改工厂类。
2.2 工厂方法模式
创建一个车工厂接口:
public interface CarFactory {
Car getCar();
}
创建一个比亚迪车工厂:
public class BydFactory implements CarFactory{
@Override
public Car getCar() {
return new Byd();
}
}
创建一个特斯拉车工厂:
public class TeslaFactory implements CarFactory {
@Override
public Car getCar() {
return new Tesla();
}
}
测试类:
public class Test {
public static void main(String[] args) {
Car car1 = new BydFactory().getCar();
Car car2 = new TeslaFactory().getCar();
car1.name();
car2.name();
}
}
依赖关系图:
优势: 相比于简单工厂,如果需要添加新的车类型,只需要创建一个对应类型的车工厂,然后使用该工厂生产某种类型的车即可。满足开放封闭原则。
缺点: 多了一层嵌套,比较繁琐。另外,如果子类越来越多,如果给每个子类都创建一个工厂,则会使系统中的类成倍增加。
3. 抽象工厂模式
定义:抽象工厂提供了一个创建一系列相关或者相互依赖对象的接口,无需指定它们具体的类,(可以理解为工厂的工厂)。为了解决工厂方法中可能越来越多的类和工厂,抽象工厂将这些产品类进行分组,有些工厂可以同时生产其中的几个产品,这样既减少了工厂的数量,同时也能保证同一工厂生产的产品具有同一工厂的统一风格。
例如:华为工厂同时生产手机和路由器,小米工厂也生产手机和路由器。避免了工厂方法模式0分别创建华为手机工厂,华为路由器工厂,小米手机工厂,小米路由器工厂这四个工厂,而是只用建立小米和华为这两个工厂即可,每个工厂负责多种产品,比如华为生产的产品分别叫“华为手机”和“华为路由器”,也保证了产品的一致性。
适用场景:
1.客户端(应用层)不依赖于产品类实例如何被创建、实现等细节
2.强调一系列相关的产品对象(属于同一产品族)一起使用创建对象需要大量的重复代码
3.提供一个产品类的库,所有产品以相同接口出现,从而使客户端不依赖于具体的实现。
优点:
1.可以确保同一工厂生成的产品相互匹配。
2.可以避免客户端和具体产品代码的耦合。
3.单一职责原则。 可以将产品生成代码抽取到同一位置, 使得代码易于维护。
4.开闭原则。 容易扩展厂商,但是不能扩展产品。
缺点:
由于采用该模式需要向应用中引入众多接口和类,代码可能会比之前更加复杂。
实例:
手机产品接口:
public interface IphoneProduct {
void openPhone();
void shutDown();
void call();
void sendMES();
}
手机实体类:
//小米手机类
class XiaomiPhone implements IphoneProduct {
@Override
public void openPhone() {
System.out.println("打开小米手机");
}
@Override
public void shutDown() {
System.out.println("关闭小米手机");
}
@Override
public void call() {
System.out.println("使用小米手机打电话");
}
@Override
public void sendMES() {
System.out.println("使用小米手机发短信");
}
}
//华为手机类
class HuaWeiPhone implements IphoneProduct {
@Override
public void openPhone() {
System.out.println("打开华为手机");
}
@Override
public void shutDown() {
System.out.println("关闭华为手机");
}
@Override
public void call() {
System.out.println("使用华为手机打电话");
}
@Override
public void sendMES() {
System.out.println("使用华为手机发短信");
}
}
路由器产品接口:
public interface IRouterProduct {
void openWifi();
void shutDown();
}
路由器实体类:
//华为路由器
class HuaWeiRouter implements IRouterProduct {
@Override
public void openWifi() {
System.out.println("打开华为WiFi");
}
@Override
public void shutDown() {
System.out.println("关闭华为路由器");
}
}
//小米路由器实体类
class XiaomiRouter implements IRouterProduct {
@Override
public void openWifi() {
System.out.println("打开小米WiFi");
}
@Override
public void shutDown() {
System.out.println("关闭小米路由器");
}
}
产品生产工厂接口:
public interface ProductFactory {
IphoneProduct getPhone();
IRouterProduct getRouter();
}
产品生产具体工厂类:
//华为工厂
class HuaweiFactory implements ProductFactory {
@Override
public IphoneProduct getPhone() {
return new HuaWeiPhone();
}
@Override
public IRouterProduct getRouter() {
return new HuaWeiRouter();
}
}
//小米工厂
class XiaomiFactory implements ProductFactory {
@Override
public IphoneProduct getPhone() {
return new XiaomiPhone();
}
@Override
public IRouterProduct getRouter() {
return new XiaomiRouter();
}
}
客户端测试类:
public class Client {
public static void main(String[] args) {
ProductFactory factory;
System.out.println("========华为=========");
factory = new HuaweiFactory();
IphoneProduct phone = factory.getPhone();
IRouterProduct router = factory.getRouter();
phone.openPhone();
phone.call();
phone.sendMES();
phone.shutDown();
router.openWifi();
router.shutDown();
System.out.println("========小米=========");
factory = new XiaomiFactory();
phone = factory.getPhone();
router = factory.getRouter();
phone.openPhone();
phone.call();
phone.sendMES();
phone.shutDown();
router.openWifi();
router.shutDown();
}
}
4. 构造者模式
建造者模式(Builder Pattern)使用多个简单的对象一步一步构建成一个复杂的对象。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
适用场景:当一个类的构造函数参数个数超过4个,而且这些参数有些是可选的参数,考虑使用构造者模式。
4.1 传统Builder 模式
构建者模式UML图如下所示:
如上图所示,builder模式有4个角色。
Product:最终要生成的对象,例如 Computer实例。
Builder: 构建者的抽象基类(有时会使用接口代替)。其定义了构建Product的抽象步骤,其实体类需要实现这些步骤。其会包含一个用来返回最终产品的方法Product getProduct()
。
ConcreteBuilder: Builder的实现类。
Director:决定如何构建最终产品的算法. 其会包含一个负责组装的方法void Construct(Builder builder)
, 在这个方法中通过调用builder的方法,就可以设置builder,等设置完成后,就可以通过builder的 getProduct()
方法获得最终的产品。
样例代码:
第一步:构造Computer类作为产品
public class Computer {
private String cpu; //构造时必选参数
private String ram; //必选
private int usbCount; //可选
private String keyboard; //可选
private String display; //可选
public Computer(String cpu,String ram){
this.cpu = cpu;
this.ram = ram;
}
public void setCpu(String cpu) {
this.cpu = cpu;
}
public void setDisplay(String display) {
this.display = display;
}
public void setRam(String ram) {
this.ram = ram;
}
public void setKeyboard(String keyboard) {
this.keyboard = keyboard;
}
public void setUsbCount(int usbCount) {
this.usbCount = usbCount;
}
@Override
public String toString() {
return "Computer{" +
"cpu='" + cpu + '\'' +
", ram='" + ram + '\'' +
", usbCount=" + usbCount +
", keyboard='" + keyboard + '\'' +
", display='" + display + '\'' +
'}';
}
}
第二步:构造一个建造者(抽象类)
//建造者,用来使用可选参数构造产品
public abstract class ComputerBuilder {
public abstract void setUsbCount(int count);
public abstract void setKeyboard(String keyboard);
public abstract void setDisplay(String display);
public abstract Computer getComputer();
}
第三步:构造真正的建造者(构造者抽象类的实现类)
//戴尔电脑构造者
class DellComputerBuilder extends ComputerBuilder {
private Computer computer;
public DellComputerBuilder(String cpu,String ram){
computer = new Computer(cpu,ram);
}
@Override
public void setUsbCount(int count) {
computer.setUsbCount(count);
}
@Override
public void setKeyboard(String keyboard) {
computer.setKeyboard(keyboard);
}
@Override
public void setDisplay(String display) {
computer.setDisplay(display);
}
@Override
public Computer getComputer() {
return computer;
}
}
//苹果电脑构造者
public class AppleComputerBuilder extends ComputerBuilder {
private Computer computer;
public AppleComputerBuilder(String cpu,String ram){
computer = new Computer(cpu,ram);
}
@Override
public void setUsbCount(int count) {
computer.setUsbCount(count); //设置默认值
}
@Override
public void setKeyboard(String keyboard) {
computer.setKeyboard(keyboard);
}
@Override
public void setDisplay(String display) {
computer.setDisplay(display);
}
@Override
public Computer getComputer() {
return computer;
}
}
第三步:指导者类(Director),用来指导产品参数构造
public class Director {
public void makComputer(ComputerBuilder builder){
if(builder instanceof AppleComputerBuilder){
builder.setUsbCount(2);
builder.setDisplay("苹果显示器");
builder.setKeyboard("苹果键盘");
}else {
builder.setUsbCount(2);
builder.setDisplay("戴尔显示器");
builder.setKeyboard("戴尔键盘");
}
}
}
测试:
public class Test {
public static void main(String[] args) {
Director director = new Director();
ComputerBuilder computerBuilder = new AppleComputerBuilder("i5处理器","4G内存");
director.makComputer(computerBuilder);
Computer computer1 = computerBuilder.getComputer();
System.out.println(computer1.toString());
ComputerBuilder computerBuilder1 = new DellComputerBuilder("i5处理器","6G内存");
director.makComputer(computerBuilder1);
Computer computer2 = computerBuilder1.getComputer();
System.out.println(computer2.toString());
}
}
/*结果;
Computer{cpu='i5处理器', ram='4G内存', usbCount=2, keyboard='苹果键盘', display='苹果显示器'}
Computer{cpu='i5处理器', ram='6G内存', usbCount=2, keyboard='戴尔键盘', display='戴尔显示器'}
*/
分析:使用构造者模式,对于客户获取产品,它只需要指定cpu和ram两个必须的参数即可,其他的参数直接由构造者来构建产品,同时让指挥者指挥其他可选参数。
4.2 改进的构造者模式
上面传统的构造者模式,使用了一个指挥者来指挥可选参数的选择,按照现实意义上,应该由客户来指挥。
如何实现:
第一步:在Computer 中创建一个静态内部类 Builder,然后将Computer 中的参数都复制到Builder类中。
第二步:在Computer中创建一个private的构造函数,参数为Builder类型
第三步:在Builder中创建一个public的构造函数,参数为Computer中必填的那些参数,cpu 和ram。
第四步:在Builder中创建设置函数,对Computer中那些可选参数进行赋值,返回值为Builder类型的实例
第五步:在Builder中创建一个build()方法,在其中构建Computer的实例并返回
// 产品:电脑
public class Computer {
private String cpu; //构造时必选参数
private String ram; //必选
private int usbCount; //可选
private String keyboard; //可选
private String display; //可选
private Computer(Builder builder){
this.cpu=builder.cpu;
this.ram=builder.ram;
this.usbCount=builder.usbCount;
this.keyboard=builder.keyboard;
this.display=builder.display;
}
public static class Builder{
private String cpu;//必须
private String ram;//必须
private int usbCount;//可选
private String keyboard;//可选
private String display;//可选
public Builder(String cpu,String ram){
this.cpu = cpu;
this.ram = ram;
}
public Builder setUsbCount(int usbCount) {
this.usbCount = usbCount;
return this;
}
public Builder setKeyboard(String keyboard) {
this.keyboard = keyboard;
return this;
}
public Builder setDisplay(String display) {
this.display = display;
return this;
}
public Computer build(){
return new Computer(this);
}
}
@Override
public String toString() {
return "Computer{" +
"cpu='" + cpu + '\'' +
", ram='" + ram + '\'' +
", usbCount=" + usbCount +
", keyboard='" + keyboard + '\'' +
", display='" + display + '\'' +
'}';
}
}
测试类:
public class Test {
public static void main(String[] args) {
//在客户端使用链式调用,一步一步的把对象构建出来。
Computer computer = new Computer.Builder("新特尔","8G三星")
.setDisplay("24寸三星")
.setUsbCount(3)
.setKeyboard("罗技键盘")
.build();
System.out.println(computer.toString());
}
}
/*结果:
Computer{cpu='新特尔', ram='8G三星', usbCount=3, keyboard='罗技键盘', display='24寸三星'}
*/
不使用静态内部类的方式实现:
第一步:创建产品类
public class Computer {
private String cpu; //构造时必选参数
private String ram; //必选
private int usbCount; //可选
private String keyboard; //可选
private String display; //可选
public Computer(String cpu,String ram){
this.cpu = cpu;
this.ram = ram;
}
public void setUsbCount(int usbCount) {
this.usbCount = usbCount;
}
public void setKeyboard(String keyboard) {
this.keyboard = keyboard;
}
public void setDisplay(String display) {
this.display = display;
}
@Override
public String toString() {
return "Computer{" +
"cpu='" + cpu + '\'' +
", ram='" + ram + '\'' +
", usbCount=" + usbCount +
", keyboard='" + keyboard + '\'' +
", display='" + display + '\'' +
'}';
}
}
第二步:抽象构造者
public abstract class Builder {
//注意set的返回值不为void而是Builder
abstract Builder setUsbCount(int usbCount);
abstract Builder setKeyboard(String keyboard);
abstract Builder setDisplay(String display);
//返回构建的产品
abstract Computer build();
}
第三步:具体的构造者
public class AppleBuilder extends Builder {
Computer computer;
public AppleBuilder(String cpu,String ram){
computer = new Computer(cpu,ram);
}
@Override
Builder setUsbCount(int usbCount) {
computer.setUsbCount(usbCount);
return this;
}
@Override
Builder setKeyboard(String keyboard) {
computer.setKeyboard(keyboard);
return this;
}
@Override
Builder setDisplay(String display) {
computer.setDisplay(display);
return this;
}
@Override
Computer build() {
return computer;
}
}
第四步:测试
public class Test {
public static void main(String[] args) {
//在客户端使用链式调用,一步一步的把对象构建出来。
Builder builder = new AppleBuilder("i7CPU","12G三星内存");
Computer computer = builder.setDisplay("苹果显示器")
.setKeyboard("罗技键盘")
.setUsbCount(4)
.build();
System.out.println(computer.toString());
}
}
5. 原型模式
原型模式(Prototype Pattern)是用于创建重复的对象,同时又能保证性能。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。
应用场景:创建一个复杂的对象的时候,如果每次都通过new的方式创建代价较高,于是可以选择使用拷贝的方式获取一个一样的克隆对象。这两个对象进行修改互不影响。
浅拷贝:
第一步:创建原型类,并实现Cloneable接口,实现其中的clone()方法
public class Product implements Cloneable{
private String name;
private Date date;
public Product(String name,Date date){
this.name = name;
this.date = date;
}
public void setName(String name) {
this.name = name;
}
public void setDate(long date) {
this.date.setTime(date);
}
@Override
public String toString() {
return "Product{" +
"name='" + name + '\'' +
", date=" + date +
'}';
}
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
}
第二步:在使用时利用原型对象拷贝克隆对象
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Product product = new Product("电脑",new Date());
System.out.println(product.toString());
System.out.println(product.hashCode());
Product product1 = (Product) product.clone();
product1.setName("手机");
System.out.println(product1.toString());
System.out.println(product1.hashCode());
product1.setDate(1233543534);
System.out.println(product.toString());
System.out.println(product1.toString());
}
}
/*结果:
Product{name='电脑', date=Wed Aug 31 11:10:43 CST 2022}
1625635731
Product{name='手机', date=Wed Aug 31 11:10:43 CST 2022}
1580066828
Product{name='电脑', date=Thu Jan 15 14:39:03 CST 1970}
Product{name='手机', date=Thu Jan 15 14:39:03 CST 1970}
*/
分析:两次哈希值不同,说明已经是不同的对象,然而当克隆对象修改了date值后,原型对象值也变了,说明这是一个浅拷贝,对于类成员变量,它们仍然指向了同一个对象!
深拷贝
将上述代码中的clone()方法改写为:(即手动将原型类中的成员变量对象逐一克隆)
@Override
public Object clone() throws CloneNotSupportedException {
Object obj = super.clone();
Product product = (Product)obj;
product.date = (Date) this.date.clone();
return obj;
}
测试:
public class Test {
public static void main(String[] args) throws CloneNotSupportedException {
Product product = new Product("电脑",new Date());
System.out.println(product.toString());
System.out.println(product.hashCode());
Product product1 = (Product) product.clone();
product1.setName("手机");
System.out.println(product1.toString());
System.out.println(product1.hashCode());
product1.setDate(1233543534);
System.out.println(product.toString());
System.out.println(product1.toString());
}
}
/*结果为:
Product{name='电脑', date=Wed Aug 31 11:20:36 CST 2022}
1625635731
Product{name='手机', date=Wed Aug 31 11:20:36 CST 2022}
1580066828
Product{name='电脑', date=Wed Aug 31 11:20:36 CST 2022}
Product{name='手机', date=Thu Jan 15 14:39:03 CST 1970}
*/
分析:副本修改了date,不影响原型中date对象,说明date对象不再是同一个。
6. 适配器模式
适配器模式(Adapter Pattern)是作为两个不兼容的接口之间的桥梁。这种类型的设计模式属于结构型模式,它结合了两个独立接口的功能。
这种模式涉及到一个单一的类,该类负责加入独立的或不兼容的接口功能。
应用场景: 客户需要的产品与提供商提供的产品没有很好匹配,就可以使用适配器模式,利用提供商提供的产品经过包装适配成客户需要的产品。
代码示例
第一步:创建用户需要的产品类。(一般是抽象类/接口)
//目标:需要一个能产生5v电压的产品
public interface Target {
int outPut5V();
}
第二步:创建提供商提供的产品类(实体类)
//提供的商品:能产生220v电压的产品
public class Adaptee {
int outPut220(){
int a = 220;
System.out.println(String.format("输出%s电压", a));
return a;
}
}
第三步:创建一个适配器,适配器实现目标产品接口。继承/组合提供的产品类。
public class Adapter implements Target{
Adaptee adaptee; //组合一个产品类
public Adapter(Adaptee adaptee){
this.adaptee = adaptee;
}
@Override
public int outPut5V() {
int a = adaptee.outPut220(); //将产品类进过一定封装修改,成为用户需要的目标类
a = a/44;
System.out.println(String.format("工作输出%s电压", a));
return a;
}
}
第四步:测试
public static void main(String[] args) {
Adaptee adaptee = new Adaptee();//创建产品
Target target = new Adapter(adaptee);//适配产品
target.outPut5V();
}
/*结果:
输出220电压
工作输出5电压
*/
注意事项:适配器不是在详细设计时添加的,而是解决正在服役的项目的问题。
7. 桥接模式
桥接(Bridge)是用于把抽象化与实现化解耦,使得二者可以独立变化。这种类型的设计模式属于结构型模式,它通过提供抽象化和实现化之间的桥接结构,来实现二者的解耦。
主要解决:在有多种可能会变化的情况下,用继承会造成类爆炸问题,扩展起来不灵活。 例如,不同的厂商制造不同的产品,每一种产品组合起来都是一个类,没增加一个厂商类就增加了很多。为了解决这种问题,引入桥接模式。
实例代码
第一步:抽象厂商类
public abstract class Band {
abstract void info();
}
class Lenovo extends Band{
@Override
void info() {
System.out.print("联想");
}
}
class Apple extends Band{
@Override
void info() {
System.out.print("苹果");
}
}
class Dell extends Band{
@Override
void info() {
System.out.print("戴尔");
}
}
第二步:抽象产品类
public abstract class Computer {
protected Band band;
abstract void info();
public Computer(Band band){
this.band = band;
}
}
class Desktop extends Computer{
public Desktop(Band band) {
super(band);
}
@Override
void info() {
band.info();
System.out.print("台式机");
}
}
class Laptop extends Computer{
public Laptop(Band band) {
super(band);
}
@Override
void info() {
band.info();
System.out.print("笔记本");
}
}
class Table extends Computer{
public Table(Band band) {
super(band);
}
@Override
void info() {
band.info();
System.out.print("平板");
}
}
第三步:测试,并两两组合
public class Test {
public static void main(String[] args) {
Computer computer = new Laptop(new Lenovo());
computer.info();
System.out.println();
Computer computer1 = new Table(new Apple());
computer1.info();
}
}
/*
联想笔记本
苹果平板
*/
8. 代理模式
在代理模式(Proxy Pattern)中,一个类代理另一个类的功能。这种类型的设计模式属于结构型模式。
应用场景:由于职责单一原则,原本的对象可能只负责某一种功能,于是可以使用代理,在原来的基础上新增加一些功能。
代码示例
第一步:创建被代理的对象
public class Host {
public void rent(){
System.out.println("租房子");
}
}
第二步:创建代理的对象
interface Rent {//定义一个行为接口
void rent();
}
public class Proxy implements Rent{
private Host host;
public Proxy(Host host) {
this.host = host;
}
@Override
public void rent(){
look();
host.rent();
HeTong();
}
public void look(){
System.out.println("看房");
}
public void HeTong(){
System.out.println("签合同");
}
}
第三步:测试
public class Test {
public static void main(String[] args) {
Host host = new Host();
Proxy proxy = new Proxy(host);
proxy.rent();
}
}
/*结果:
看房
租房子
签合同
*/
9. 装饰器模式
装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其结构。这种类型的设计模式属于结构型模式,它是作为现有的类的一个包装。
这种模式创建了一个装饰类,用来包装原有的类,并在保持类方法签名完整性的前提下,提供了额外的功能。
应用场景:一般的,我们为了扩展一个类经常使用继承方式实现,由于继承为类引入静态特征,并且随着扩展功能的增多,子类会很膨胀。在不想增加很多子类的情况下扩展类,可以使用装饰器模式。
实例
第一步:先创建一个现有对象的接口,方便现有对象的扩展
public interface shape {
void draw();
}
第二步:创建现有对象的实体类,该类就是被装饰的对象
//基础的圆、长方形类
class Circle implements shape{
@Override
public void draw() {
System.out.println("画圆形");
}
}
class Rectangle implements shape{
@Override
public void draw() {
System.out.println("画长方形");
}
}
第三步:创建修饰器接口(抽象类),方便对修饰器进行扩展。在抽象类中组合一个被修饰对象。
public abstract class ShapeDecorator{
protected shape decoratorShape;
public ShapeDecorator(shape decoratorShape) {
this.decoratorShape = decoratorShape;
}
public abstract void draw();
}
第四步:创建修饰器实体类
class RedShapeDecorator extends ShapeDecorator {
public RedShapeDecorator(shape decoratorShape) {
super(decoratorShape);
}
@Override
public void draw() {
decoratorShape.draw();
setRedBorder(decoratorShape);//对原有方法进行修饰
}
private void setRedBorder(shape decoratedShape){
System.out.println("边框颜色:红色");
}
}
class YellowShapeDecorator extends ShapeDecorator {
public YellowShapeDecorator(shape decoratorShape) {
super(decoratorShape);
}
@Override
public void draw() {
decoratorShape.draw();
setRedBorder(decoratorShape); //对原有方法进行修饰
}
private void setRedBorder(shape shape){
System.out.println("画框颜色:黄色");
}
}
第五步:测试
public class Test {
public static void main(String[] args) {
shape shape = new Circle();
ShapeDecorator decorator = new RedShapeDecorator(shape);
decorator.draw();
System.out.println("=================");
ShapeDecorator decorator1 = new RedShapeDecorator(new Rectangle());
decorator1.draw();
System.out.println("=================");
ShapeDecorator decorator2 = new YellowShapeDecorator(new Rectangle());
decorator2.draw();
}
}
/*结果:
画圆形
边框颜色:红色
=================
画长方形
边框颜色:红色
=================
画长方形
画框颜色:黄色
*/