文章目录
Java面向对象编程
学习目标:
1.理解面向对象思想和概念
2.学会面向对象分析方法
3.掌握类、对象、继承、多态的使用
4.熟悉重写、重载、抽象类、内部类以及设计模式的应用
知识点列表:
类、对象、属性、方法、构造器
封装、继承、多态
重写、重载
访问权限控制符
this和super、static、final
抽象类和接口,内部类
各种设计模式
Java设计原则
前言:
在 Java 的世界里,万物皆对象,所以没对象的不要担心,你学 Java 可以拥有很多对象。
面向过程到面向对象思想层面的转变:
面向过程关注的是执行的过程,面向对象关注的是具备功能的对象。
面向过程到面向对象,是程序员思想上从执行者到指挥者的转变。
面向对象的三大思想:
OOA:面向对象分析(Object Oriented Analysis)
OOD:面向对象设计(Object Oriented Design)
OOP:面向对象程序(Object Oriented Programming)
面向对象三大特征:
封装性:所有的内容对外部是不可见的
继承性:将其他类的功能继承下来继续发展
多态性:方法的重载本身就是多态性的一个体现
类、对象、属性、方法、构造器
(1)类
类是一个模板,它描述一类对象的行为和状态。Java 中类表示一个共性的产物,代表一个综合的特征。
类的定义:
class 类名称 {
成员属性
成员方法
}
类必须通过对象才可以使用,但是对象的所有操作都定义在类中。
类由方法和属性组成,其中,属性相当于特征,而方法相当于行为。
(2)对象
对象是类的一个实例(并非女朋友),有状态和行为。对象表示一个个性的产物,是个体的特征。
一个类如果想要进行操作,必须要依靠对象,定义格式为:
类名称 对象名称 = new 类名称();
如果想要访问类中的属性或者方法,则可以按照如下语法:
访问类中的属性: 对象.属性;
调用类中的方法: 对象.方法(实际参数列表);
(3)属性
属性的定义格式:
数据类型 属性名;
属性定义并赋值的格式:
数据类型 属性名 = 初始化值;
(4)方法
方法定义格式:
权限修饰符 返回值类型 方法名(形式参数列表) {
//方法体
return 返回值;
}
(5)构造器
构造器用于对象的初始化,在创建对象的时候自动调用,在 Java 中,所有对象的创建都是通过 new 关键字创建,new 关键字旨在告诉 jvm 需要去开辟一块内存空间来存储对象。
每个类都至少存在一个构造方法。我们在定义类的时候如果没有显式地为类定义构造方法,Java 编译器将会为该类提供一个默认构造方法,默认构造方法中没有代码,是无参的构造方法。当然,如果我们写了自己的构造器,编译器便不再为我们生成无参的构造方法,在实际编写过程中,当类中有非常量成员变量时,建议同时提供无参构造方法和全参构造方法;如果类中所有成员变量都是常量或者没有成员变量时,建议不提供构造方法;当然有时候我们在实际需求面前,可能有时候需要同时提供多个不同参数数量的构造方法。
在创建一个对象的时候,至少要调用一个构造方法。构造方法的名称必须与类同名,一个类可以有多个构造方法。
定义的格式:
与普通方法基本相同, 区别在于: 方法名称必须与类名相同, 没有返回值类型的声明。
如:
public class User {
private String name;
private int age;
// 无参构造方法
public User(){
}
// 全参构造方法
public User(String name, int age){
this.name = name;
this.age = age;
}
}
封装、继承、多态
(1)封装性:所有的内容对外部是不可见的
封装的优点:
良好的封装能够减少耦合
类的内部可以自由修改
能够对成员变量进行精准控制
能够隐藏实现细节
实现封装的步骤:
第一步,修改属性的可见性,一般设置为 private,如:
public class User {
private String name;
private int age;
}
第二步,对每个属性提供对外访问的公共方法,即我们常说的 get and set,如:
public class User {
private String name;
private int age;
public String getName(){
return name;
}
public int getAge(){
return age;
}
public void setName(String name){
this.name = name;
}
public void setAge(int age){
this.age = age;
}
}
(2)继承性:将其他类的功能继承下来继续发展
继承的声明:
在 Java 中我们可以通过 extends 关键字来声明某一个类继承另一个类,如:
class A {
}
class B extends A {
}
在子类继承父类之后,就拥有了父类当中的属性和方法,子类当中就不会存在重复的代码,提高了代码的简洁性、可维护性、复用性。
与 C++ 不同,Java 不支持多重继承, Java 中的继承是单继承,但是可以多重继承。
(3)多态性:方法的重载本身就是多态性的一个体现
多态的体现:
多态是同一个行为具有多个不同表现形式或形态的能力。对象的多态性,从概念上理解,在类中有子类和父类之分,子类就是父类的一种形态,对象的多态性由此而来。
重载是一个类当中方法的多态性体现,重写是父子类当中方法的多态性体现。
多态的优点:
消除类型之间的耦合关系
可替换性
可扩充性
接口性
灵活性
多态存在的必要条件:
继承
重写
父类的引用指向子类的对象:Father father = new Son();
多态的实现方式:
重写
接口(interface)
抽象类和抽象方法
多态的使用:
类似于基本数据类型转换。分为:
向上转型:
将子类实例变为父类实例:Father father = new Son();
向下转型:
将父类实例变为子类实例:Son son = (Son)father;
重写、重载
(1)重写(Override)
重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。重写的好处在于子类可以根据自己的需求,定义属于自己的特定行为。
重写的规则:
- 参数列表与被重写方法的参数列表必须完全相同。 返回类型与被重写方法的返回类型可以不相同,但是必须是父类返回值的派生类(java5及更早版本返回类型要一样,java7 及更高版本可以不同)。
- 访问权限不能比父类中被重写的方法的访问权限更低。例如:如果父类的一个方法被声明为 public,那么在子类中重写该方法就不能声明为protected。
- 父类的成员方法只能被它的子类重写。
- 声明为 final 的方法不能被重写。
- 声明为 static的方法不能被重写,但是能够被再次声明。
- 子类和父类在同一个包中,那么子类可以重写父类所有方法,除了声明为 private 和 final 的方法.
- 子类和父类不在同一个包中,那么子类只能够重写父类的声明为 public 和 protected 的非 final 方法。
- 重写的方法能够抛出任何非强制异常,无论被重写的方法是否抛出异常。但是,重写的方法不能抛出新的强制性异常,或者比被重写方法声明的更广泛的强制性异常,反之则可以。
- 构造方法不能被重写。
- 如果不能继承一个类,则不能重写该类的方法。
(2)重载(overload)
重载是在一个类里面,方法名称相同,而参数类型或者参数长度不同。返回值类型可以相同也可以不同。每个重载的方法(或者构造函数)都必须有一个独一无二的参数类型列表。
构造方法的重载:
一个类可以拥有多个构造方法,参数列表的长度或者类型不同即能完成构造方法的重载。构造方法的重载,可以在不同的需求下,调用不同的方法来初始化对象。
重载的规则:
- 被重载的方法必须改变参数列表(参数个数或类型不一样);
- 被重载的方法可以改变返回类型;
- 被重载的方法可以改变访问修饰符;
- 被重载的方法可以声明新的或更广的检查异常;
- 方法能够在同一个类中或者在一个子类中被重载。
- 无法以返回值类型作为重载函数的区分标准。
访问权限控制符
this 和 super、static、final
(1)this
使用 this 关键字可以完成以下几种操作:
- 调用类中的属性
- 调用类中的方法或构造方法
- 表示当前对象
(2)super
super 可以理解为是指向自己父类对象的一个指针,而这个父类指的是离自己最近的一个父类。
使用 super 关键字可以完成以下几种操作:
- 普通的直接引用,相当于直接指向当前对象的父类,可以通过 super.xx 来调用父类的成员
- 子类中的成员变量或方法与父类中的成员变量或方法同名
- 引用父类构造函数
(3)static
static 表示静态的意思,可用来修饰成员变量和成员方法,主要的作用在于创建独立于具体对象的域变量或者方法。被 static 修饰的方法或者变量不依赖对象进行访问,只要类被加载以后,就可以通过类名去访问,并且不会在内存中建立多份属性(存储在 jvm 中的方法区,为所有对象公有)。静态成员只有在类被加载的时候初始化,且静态不可以访问非静态,但是非静态可以访问静态,因为静态资源比非静态的资源先加载。
(4)final
被 final 修饰的变量存放在 jvm 中方法区中的运行时常量池。
final 变量:
final 表示"最后的、最终的"含义,变量一旦赋值后,不能被重新赋值,所以被 final 修饰的实例变量必须显式指定初始值。final 修饰符通常和 static 修饰符一起使用来创建类常量。
final 方法:
父类中的 final 方法可以被子类继承,但是不能被子类重写。声明 final 方法的主要目的是防止该方法的内容被修改。
final 类:
final 类不能被继承,没有类能够继承 final 类的任何特性。
抽象类和接口,内部类
(1)抽象类
在面向对象的概念中,所有的对象都是通过类来描绘的,但是反过来,并不是所有的类都是用来描绘对象的,如果一个类中没有包含足够的信息来描绘一个具体的对象,这样的类就是抽象类。
抽象类必须使用 abstract class 进行声明,一个抽象类中可以没有抽象方法,但是抽象方法必须写在抽象类或者接口中。
抽象类本身并不能直接实例化(不能通过 new 关键字实例化),且一个抽象类必须被子类继承,如果子类不是抽象类必须重写抽象类中的全部抽象方法(抽象类的子类可以是抽象类,但是最终必须要有子类继承)。
抽象类不能用 final 关键字进行修饰,因为 final 修饰的类不能被继承,但是抽象类必须有子类继承才有意义。
抽象类也可以有构造方法,子类对象实例化的流程与普通类的继承是一样的,都需要先调用父类当中的构造方法,之后再调用子类自己的构造方法。
抽象类声明格式:
abstract class 类名 {
}
抽象方法:
只进行声明但是未进行实现(没有 {} 方法体)的方法,抽象方法必须使用 abstract 关键字进行声明。
格式:
abstract class 类名 {
public abstract void 方法名();
}
抽象类和普通类的区别:
- 抽象类必须用 public 或 protected 修饰(如果用 private 进行修饰,那么子类则无法继承,也就无法实现其抽象方法)。 默认缺省为 public
- 抽象类不可以使用 new 关键字创建对象, 但是在子类创建对象时, 抽象父类也会被 JVM 实例化。
- 如果一个子类继承抽象类,那么必须实现其 所有的抽象方法。如果有未实现的抽象方法,那么子类也必须定义为 abstract 类 。
(2)接口
如果一个类中的方法都是抽象方法,全部属性都是全局常量,此时可以将这个类定义为接口。接口无法实例化,但是可以被实现。一个类通过继承接口,来继承接口的抽象方法。
面向接口编程的思想:
接口是定义与实现的分离。这种思想能够降低程序的耦合性,易于程序的扩展和维护。
全局常量和抽象方法的简写:
接口本身都是由全局常量和抽象方法组成的,所以全局变量在编写时,可以省略 public static final 关键字,方法可以省略 public abstract 关键字,省略后变成:
public interface A {
String INFO = "说明";
void print();
}
接口的实现:
使用 implements 关键字,接口是可以多实现的:
class 子类名称 implements 父接口1, 父接口2... {
}
一个类可以既实现接口又继承抽象类:
class 子类名称 extends 父类 implements 父接口 {
}
接口的多继承:
在 Java 中类的多继承是不合法的,但是接口都是抽象部分,不存在具体的实现,所以接口可以存在多继承。
public interface C extends A, B {
}
接口和抽象类的区别:
- 抽象类需要被子类继承,接口需要被子类实现。
- 抽象类可以既有抽象方法,又有非抽象方法,但是接口只能有抽象方法。
- 设计理念不同,抽象类表示的是“is-a”,而接口表现的是“has-a”。
- 抽象类的变量是普通变量,接口中的变量只能是 public static final。
- 抽象类表示的是一种继承关系,无法多继承,但是接口可以多实现。
- 抽象类中可以包含 static 方法,但是接口不可以,因为静态方法不能被子类重写。
- 抽象类可以有构造方法,但是接口不能有构造方法。
(3)内部类
看到这个名词,莫名的想到——“累不累”,真的很累。
在 Java 中,可以将一个类定义在另一个类里面或者方法里面,这样的类称为内部类。
成员内部类:
成员内部类定义在另一个类的内部:
class Outer {
private double x = 0;
public Outer(double x) {
this.x = x;
}
class Inner { //内部类
public void print() {
System.out.println("x=" + x); // x=0
}
}
}
成员内部类可以无条件访问外部类所有的属性和方法,包括私有方法。
当成员内部类中的成员变量或方法与外部类同名时,会发生隐藏现象,即采用就近原则,默认访问的是成员内部类的成员变量或者方法。如果要对外部类的属性或者方法进行访问,应该采用: 外部类.this.属性(方法)
外部类访问内部类:
Outter outter = new Outter();
Outter.Inner inner = outter.new Inner();
局部内部类:
局部内部类定义在一个方法或者一个作用域内,访问仅限于方法内或者该作用域内。如同方法里的一个局部变量,不能有 public、protected、private、static 修饰符。
class Person {
public Person() {
}
}
class Man {
public Man() {
}
public Person getPerson() {
class Student extends Person { //局部内部类
int age = 0;
}
return new Student();
}
}
匿名内部类:
匿名内部类没有名字。
创建格式:
new 父类构造器(参数列表)| 实现接口() {
//匿名内部类的类体部分
}
- 匿名内部类必须要继承一个父类或者实现一个接口,且只能继承一个父类或者实现一个接口,二者不可得兼。
- 匿名内部类没有 class 关键字。
- 匿名内部类不能定义构造函数。
- 不能存在任何静态成员变量和静态方法。
- 匿名内部类也属于局部内部类,所以局部内部类的限制对匿名内部类有效。
- 不能是抽象的,且需要实现继承类或者实现的接口的全部抽象方法。
- 只能够访问 final 类型的局部变量。
静态内部类:
静态内部类是定义在另一个类里面的用 static 关键字修饰的类,不需要依赖于外部类的对象,不能访问外部类的非 static 成员变量或者方法(与 static 修饰的属性或者方法类似)。
class Outer {
public Outer() {
}
static class Inner { //静态内部类
}
}
// 访问方法:
// Outter.Inner inner = new Outter.Inner();
各种设计模式
设计模式之创建型模式:
(1)单例模式
定义:
确保某一个类只有一个实例,而且自行实例化并向整个系统提供。
单例模式的分类:
懒汉式:
public class LazyMode {
// 保证线程同步
private static volatile LazyMode lazyMode = null;
// 构造方法私有,避免在类的外面创建对象
private LazyMode() {
}
/**
当类加载的时候并不会生成单例,只有第一次调用 getInstance 方法的时候才会创建这个单例,关键字 volatile 和 synchronized 虽然能保证线程的安全,但是每次访问都要进行同步,会影响系统的性能,消耗更多的资源。
*/
public static synchronized LazyMode getInstance() {
if (lazyMode == null) {
lazyMode = new LazyMode();
}
return lazyMode;
}
}
饿汉式:
public class HungryMode {
// 实例化单例
private static final HungryMode hungryMode = new HungryMode();
private HungryMode() {
}
/**
当类加载的时候就创建一个单例,在调用 getInstance 方法之前就已经存在了,且该方式线程安全。
*/
public static HungryMode getInstance() {
return hungryMode;
}
}
单例模式的使用场景:
- 某个类只要求生成一个对象的时候。
- 当对象需要被共享的时候。
- 某个类需要频繁实例化,且创建的对象又频繁的被销毁的时候,比如:多线程的线程池。
单例模式的优缺点:
优点:
- 内存中只有一个实例,减少了内存开销,特别是在频繁创建和销毁的场景下。
- 避免了对资源的多重占用。
- 单例模式可以设置系统的全局访问点,优化和共享资源的访问。
缺点:
- 单例模式一般没有接口,难以扩展。
- 与单一职责冲突。一个类应该只关心内部逻辑,而不关心外面是如何实例化的。
(2)工厂模式
定义:
定义一个用户创建对象的接口,让子类决定实例化哪一个类。工厂方法让一个类的实例化延迟到了它的子类。
主要角色:
抽象工厂:提供创建产品的接口,调用者通过接口访问具体工厂的工厂方法。
具体工厂:实现抽象工厂的工厂方法,完成具体产品的创建。
抽象产品:定义产品的规范,描述产品的主要性能和特征。
具体产品:实现抽象产品所定义的接口。
工厂方法模板:
//抽象产品:提供了产品的接口
public interface Product {
public void method();
}
//具体的产品可以有多个,都实现抽象产品接口
public class ConcreteProduct1 implements Product {
public void method() {
//具体业务逻辑处理,例如
System.out.println("具体产品1...");
}
}
public class ConcreteProduct2 implements Product {
public void method() {
//具体业务逻辑处理,例如
System.out.println("具体产品2...");
}
}
//抽象工厂:负责定义产品对象的产生
public abstract class AbstractFactory {
//创建一个产品对象,输入的参数类型可以自行设置
public abstract T createProduct(Class tClass);
}
//具体工厂:具体如何生产一个产品的对象,是由具体的工厂类实现的
public class ConcreteFactory implements AbstractFactory {
public T createProduct(Class tClass) {
Product product = null;
try {
product = (T)Class.forName(tClass.getName()).newInstance();
} catch (Exception e) {
//异常处理
}
return (T)product;
}
}
工厂模式的使用场景:
- 客户只知道创建产品的工厂名,不知道具体的产品名。
- 创建对象的任务由多个具体子工厂中的某一个完成,抽象工厂只负责提供接口。
- 客户不关心创建产品的细节,只关心产品的品牌。
(3)抽象工厂模式
定义:
为创建一组相关或者相互依赖的对象提供一个接口,而且无需指定它们的具体类。抽象工厂模式是工厂模式的升级版,它能为在有多个业务品种、业务分类时提供非常好的解决方案。
使用抽象工厂模式一般要满足的条件:
系统中有多个产品族,每个具体的工厂创建同一族但是属于不同等级结构的产品。
系统一次只可能消费其中某一族产品。
抽象工厂的模板:
// 抽象产品类(只写了一个AbstractProductA,AbstractProductB省略):
public abstract class AbstractProductA {
//每个产品的共有方法
public void sharedMthod() { }
//每个产品相同方法,不同实现
public abstract void doSomething();
}
// 具体产品类:
public class ProductA1 extends AbstractProductA {
public abstract void doSomething() {
System.out.println("产品A1的实现方法");
}
}
public class ProductA2 extends AbstractProductA {
public abstract void doSomething() {
System.out.println("产品A2的实现方法");
}
}
//抽象工厂类:
public abstract class AbstractCreator {
//创建A产品家族
public abstract AbstractProductA createProductA();
//创建B产品家族
public abstract AbstractProductB createProductB();
//如果有N个产品族,该类中应该有N个创建方法
}
//产品等级实现类:
//有M个产品等级就应该有M个工厂的实现类,在每个实现工厂中,实现不同产品族的生产业务。
public class Creator1 extends AbstractCreator {
//只生成产品等级为1的A产品
public AbstractProductA createProductA() {
return new ProductA1();
}
//只生成产品等级为1的B产品
public AbstractProductB createProductB() {
return new ProductB1();
}
}
public class Creator2 extends AbstractCreator {
//只生成产品等级为2的A产品
public AbstractProductA createProductA() {
return new ProductA2();
}
//只生成产品等级为2的B产品
public AbstractProductB createProductB() {
return new ProductB2();
}
}
抽象工厂的优点:
- 可以在类的内部对产品族中相关联的多等级产品共同管理。
- 当增加一个新的产品族时不需要修改源代码。
抽象工厂的缺点:
当新增一个产品族中的产品时,所有的工厂类都需要修改。
应用场景:
- 适用于产品之间相互关联、相互依赖且相互约束的地方。
- 需要动态切换产品族的地方。
(4)建造者模式
定义:
将一个复杂对象的构造与它的表示分离,从而同样的构建过程可以有不同的表示,这样的设计模式称为建造者模式。它将一个对象分解为多个简单的对象,然后一步步构建,也就是说产品的组成部分是不变的,但是每一部分我们可以灵活选择。
建造者模式的结构:
产品类:包含多个组成部件的复杂对象,由具体建造者来创建各个部件。
抽象建造者:它是一个包含创建产品各个子部件的抽象方法的接口,通常还包含一个返回复杂产品的方法。
具体建造者:实现抽象建造者的接口,完成复杂产品的各个部件的具体创建方法。
导演类:调用建造者的部件构造与装配方法完成复杂对象的创建,不涉及具体产品的信息。
优点:
- 各个具体的建造者相互独立,有利于系统的独立。
- 客户端不需要知道产品内部组成的细节,便于控制细节风险。
应用场景:
-相同的方法,不同的执行顺序,产生不同的实践结果。
- 创建的对象比较复杂,由多个部件构成。
- 产品的构建过程和最终的表示是独立的。
设计模式之结构性模式:
(1)代理模式
定义:
为其他对象提供一种代理以控制这个对象的访问。
结构:
抽象主题角色:可以是接口或者抽象类,是一个普通的业务类型定义,声明真实主题和代理对象实现的业务方法。
被代理角色:实现抽象主题中的具体业务,是代理对象所代表的真实对象,是最终要引用的对象,是业务逻辑的具体执行者。
代理类:负责对真实角色的应用,把抽象主题类定义的方法限制委托给被代理对象去实现,并且在被代理对象实现前后做预处理和善后。
模板代码:
//抽象主题类
public interface Subject {
void request();
}
//真实主题类
public class RealSubject implements Subject {
public void request() {
//业务逻辑处理
}
}
//代理类:代理模式的核心就在代理类上
public class Proxy implements Subject {
//要代理哪个实现类
private RealSubject subject = null;
//通过构造方法传入被代理对象(也可以有其他方式)
public Proxy(RealSubject subject) {
this.subject=subject;
}
public void request() {
preRequest();
subject.request();
postRequest();
}
//预处理
public void preRequest() {
System.out.println("访问真实主题之前的预处理。");
}
//善后工作
public void postRequest() {
System.out.println("访问真实主题之后的善后。");
}
}
优点:
- 代理模式能在客户端与目标对象之间起到一个中介作用和保护目标对象的作用。
- 代理对象可以扩展目标对象的功能。
- 代理模式能将客户端与目标对象分离,一定程度上降低了系统的耦合度。
(2)适配器模式
定义:
将一个类的接口转换为客户希望的另一个接口,使得原来由于接口不兼容而不能在一起工作的类能在一起工作。
结构:
- 目标角色:该角色定义把其他类转换为何种接口,即期望接口。
- 源角色:被转换对象。
- 适配器角色:将原角色转换成目标角色。
模板代码:
//目标角色
public interface Target {
public void request();
}
//源角色
public class Adaptee {
public void doSomething() {
System.out.println("源角色dosomething");
}
}
//适配器角色
public class Adapter extends Adaptee implements Target {
public void request() {
super.doSomething();
}
}
//场景类:
public class Client {
public static void main(String[] args) {
Target target = new Adapter();
target.request();
}
}
优点:
- 客户端可以通过适配器透明去调用目标接口。
- 复用了现有的类。
- 将目标类和适配者进行了解耦,解决了目标类和适配者接口不一致的问题。
(3)装饰者模式
定义:
在不改变现有对象的情况下,动态地给该对象增加一些职责的模式。
结构组成:
抽象构件角色:是一个抽象类或者接口,是定义最核心的对象,也就是原始对象。
具体构件角色:实现抽象构件,通过装饰角色添加一些职责。
抽象装饰角色:一般是一个抽象类,继承抽象构件,实现其抽象方法。
具体装饰角色:实现抽象装饰的相关方法,并给具体构件对象添加职责。
模板代码:
//抽象构件
abstract class Component {
public abstract void operation();
}
//具体构件
public class ConcreteComponent extends Component {
@Override
public void operation() {
System.out.println("具体对象的操作");
}
}
//抽象装饰者
public abstract class Decorator extends Component {
private Component component = null;
//通过构造函数传递给被修饰者
public Decorator(Component component) {
this.component = component;
}
//委托给被修饰者执行
@Override
public void operation() {
if(component != null) {
this.component.operation();
}
}
}
//具体装饰者
public class ConcreteDecoratorA extends Decorator {
//定义被修饰者
public ConcreteDecoratorA(Component component) {
super(component);
}
//定义自己的修饰方法
private void method1() {
System.out.println("method1 修饰");
}
@Override
public void operation() {
this.method1();
super.operation();
}
}
public class ConcreteDecoratorB extends Decorator {
//定义被修饰者
public ConcreteDecoratorB(Component component) {
super(component);
}
//定义自己的修饰方法
private void method2() {
System.out.println("method2 修饰");
}
@Override
public void operation() {
super.operation();
this.method2();
}
}
//场景类
public class Client {
public static void main(String[] args) {
Component component = new ConcreteComponent();
//第一次修饰
component = new ConcreteDecoratorA(component);
//第二次修饰
component = new ConcreteDecoratorB(component);
//修饰后运行
component.operation();
}
}
优点:
- 采用装饰模式是扩展对象比采用继承更加灵活。
- 可以设计出多个不同的具体装饰类,创造出多个不同行为的组合。
装饰模式在 Java 中的应用:Java I/O 标准库的设计。
Java设计原则
(1)开闭原则:Open Closed Principle。
定义:
软件实体应当对扩展开放,对修改关闭。即一个软件应该通过扩展来变化,而不是去修改已有的代码。
我们在遇到业务需求变化的时候,可以通过扩展实现变化。增加一个子类,重写父类的方法,高层次的模块通过新增加的子类产生新的对象,完成业务变化对系统的最小开发,这样修改,风险也比较小。
开闭原则的作用:
- 软件如果遵守开闭原则,我们在测试的时候,只需要对扩展的代码进行测试。
- 可以提高代码的复用性。
- 可以提高软件的可维护性。
(2)单一职责原则:Single Responsibility Principle
定义:
单一职责原则又称单一功能原则。职责指变化的原因,单一职责原则规定一个类应该有且只有一个引起它变化的原因,否则类应该被拆分。
一个对象如果承担了太多职责,则会有如下缺点:
- 一个职责的变化可能会削弱或者抑制这个类实现其他职责的能力。
- 当客户端只需要该对象的某一个职责的时候,不得不将其他不需要的职责也包含进来,从而造成代码冗余。
优点:
单一职责原则的核心是控制类的粒度的大小、对对象解耦,提高内聚性。如果遵循单一原则,则会有如下优点:
- 降低类的复杂度。
- 提高类的可读性。
- 提高系统的可维护性。
- 降低变更引起的风险。
单一职责同样也实用于方法,一个方法应当尽可能做好一件事,且当一个方法行数不宜超过80行,否则应当进行抽离。
(3)里氏替换原则:Liskov Substitution Principle
该原则可以理解为:子类可以替换父类。
定义:
- 第一种:如果每一个类型为 S 的对象 o1,都有一个类型 T 的对象 o2,在以 T 定义的所有程序 P 中将所有的对象 o2 都替换为 o1,而程序 P 的行为没有发生任何变化,那么 S 是 T 的子类。
- 第二种:所有引用基类的地方必须能够透明地使用其子类对象。
第一种定义看起来很拗口,但是如果我们仔细一想,就不难理解其意思。
定义包含的含义:
- 子类必须完全实现父类的方法。
- 子类中可以增加自己特有的方法。
- 当子类覆盖或实现父类的方法时,方法的形参要比父类方法的形参更宽松。(这个得仔细想一下才能明白,并非重写了,而是进行了重载,当形参为父类形参类型时,只会执行父类的方法,不会执行父类的重载方法)
- 当子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格。
作用:
- 里氏替换原则是实现开闭原则的重要方式之一。
- 克服了继承中重写父类造成的可复用性变差。
- 类的扩展不会给已有的系统造成错误,降低了代码出错的可能性。
(4)依赖倒置原则:Dependence Inversion Principle
定义:
高层模块不应该依赖于底层模块,两者都应该依赖其抽象,抽象不应该依赖细节,细节应该依赖抽象。
依赖倒置原则的核心思想是要面向接口编程而不是面向实现编程。
作用:
- 降低类之间的耦合性。
- 提高系统的稳定性。
- 减少并行开发引起的风险。
- 提高代码的可读性和可维护性。
实现方法:
- 每个类尽量提供接口或抽象类,或者两者都具备。
- 变量的声明类型尽量是接口或者是抽象类。
- 任何类都不应该从具体类派生。
- 尽量不要覆写基类的方法
- 使用继承时结合里氏替换原则。
(5)接口隔离原则:Interface Segregation Principle
定义:
- 第一种:客户端不应该被迫依赖于它不使用的方法。
- 第二种:一个类对另一个类的依赖应该建立在最小的接口上。
优点:
- 将庞大的接口拆分,可以预防外来变更的扩散,提高系统的灵活性和可维护性。
- 提高了系统的内聚性,降低了耦合性。
- 接口大小设计合理,能够保证系统的稳定性。
- 多个专门的接口能体现对象的层次,因为可以通过对接口的继承,实现对总接口的定义。
- 能减少项目中的冗余代码。
实现接口隔离的方法:
- 接口应该尽量小,但是要有限度,一个接口应当只服务于一个子模块或者业务逻辑。
- 只提供调用者需要的方法,屏蔽那些不需要的方法。
- 在深入了解业务逻辑的情况下制定拆分接口的标准。
- 提高内聚,减少对外的交互。
(6)迪米特法则:Law of Demeter
定义:
迪米特法则又叫最少知识原则,要求一个对象应该对其他对象有最少的了解。一个类对自己需要调用的类应当知道的最少,即我只需要知道你提供的 public 方法,而你内部的实现细节是与我无关的。迪米特法则还有一个定义:只与你的直接朋友交谈,不跟陌生人说话。目的是为了降低类之间的耦合度,提高各个模块之间的相对独立性。
优点:
- 降低了类之间的耦合度,提高了各个模块之间的相对独立性。
- 提高了类的可复用率和系统的扩展性。
实现方法:
- 在类的划分上,创建弱耦合的类。
- 在类结构设计上,尽量降低类成员的访问权限。
- 优先考虑将类设计成不变类。
- 对一个类进行引用时,尽量减少引用次数。
- 尽量不暴露属性,而是提供 set and get。
- 谨慎使用序列化。