Java OOP
Java 相比于其他面向对象语言(比如C++)在设计上很大的不同,准确理解 Java 的基本语法元素是写好Java程序的第一步。
面向对象三大特点:封装、继承、多态。封装隐藏了事务内部的实现细节,提供了合理的边界,避免外部调用者接触到内部细节。继承为了代码复用,它是紧耦合的,父类代码修改,子类行为也会跟着变。实践中过度滥用继承可能会起到反效果。多态为了接口复用,一个接口,多种实现。这里可能会立即联想到重写(override)和重载(overload)、向上转型。重写是父子类中相同名字和参数的方法,不同的实现;重载则是相同名字的方法,但是不同的参数,本质上这些方法签名是不一样的。特别说明的一点,方法名称和参数一致,但返回值不同,不是有效的重载,这点Java和C++一致,编译都会报错。
Java的权限控制相对严格,引入 package 包,有更好的封装性,没有像C++中的 friend 关系来提供更宽松的访问权限。通过关键字private、default(不写任何关键字)、protected和public来控制类、方法和变量的访问权限。类(包括抽象类)只有单继承,没有 C++ 中的多重继承。接口 interface 可以实现多继承,具体差异和C++类机制比较。
public class Parent {private int privateField = 0; // 只能在Parent类内部访问int packageField = 0; // 默认权限,同一个包内可以访问protected int protectedField = 0; // 子类和同一个包内可以访问public int publicField = 0; // 任何地方都可以访问private void privateMethod() { // 只能在Parent类内部调用System.out.println("privateMethod");}void packageMethod() { // 默认权限,同一个包内可以调用System.out.println("packageMethod");}protected void protectedMethod() { // 子类和同一个包内可以调用System.out.println("protectedMethod");}public void publicMethod() { // 任何地方都可以调用System.out.println("publicMethod");}
}public class Child extends Parent {void test() {int i = privateField; // 错误:不能从Parent类外部访问privateFieldi = packageField; // 错误:默认权限不包括子类i = protectedField; // 正确:子类可以访问i = publicField; // 正确:任何地方都可以访问privateMethod(); // 错误:不能从Parent类外部调用privateMethodpackageMethod(); // 错误:默认权限不包括子类protectedMethod(); // 正确:子类可以调用publicMethod(); // 正确:任何地方都可以调用}
}
接口是对行为的抽象,它是抽象方法的集合(声明性的),方法定义和实现分离。接口不能实例化;不能包含任何非常量成员,任何 field 都是隐含着 public static final 的意义;没有非静态方法实现(要么抽象方法,要么静态方法,先不考虑 default method 新特性)。比如java.util.List。
抽象类是不能实例化的类,用 abstract 修饰 class,主要为了代码重用。除了不能实例化,形式上和一般Java类没有太大区别,可以有一个或者多个抽象方法,也可以没有抽象方法。抽象类大多用于抽取Java类的共用方法或共同成员变量,通过继承实现代码复用,比如java.util.AbstractList。
Java类实现 interface 使用 implements 关键词,继承 abstract class 使用 extends 关键词。
public class ArrayList<E> extends AbstractList<E>implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//...
}
注:
1. 有些特定场景需要抽象出与具体实现、实例化无关的通用逻辑,或者纯调用关系,使用传统抽象类会陷入到单继承的窘境,常见做法是由静态方法组成工具类(Utils),比如java.util.Collections。
2. 为接口添加任何抽象方法,所有实现了这个接口的类必须实现新增方法,否则会编译错误。对于抽象类,如果我们添加非抽象方法,其子类只会享受到能力扩展,不用担心编译出问题。
3. 接口的职责也不仅仅限于抽象方法的集合,也有各种不同的实践。有一类没有任何方法的接口,通常叫作 Marker Interface,它是为了声明某些东西,比如Cloneable、Serializable等。这种用法也存在于业界其他Java产品代码中。这似乎和Annotation异曲同工,但它的好处是简单直接。因为Annotation可以指定参数和值,在表达能力上要更强大,更多人选择使用Annotation。
4. Java 8 以后接口也可以有方法实现。从 Java 8 开始,interface 增加对 default method 的支持。Java 9 以后甚至可以定义 private default method。Default method 提供了一种二进制兼容扩展已有接口的办法,比如 java.util.Collection,它是 collection 体系的 root interface,在 Java 8 中添加了一系列 default method,主要是增加 Lambda、Stream 相关的功能。类似 Collections 的工具类,很多方法都适合作为 default method 实现在基础接口里面。
public interface Collection<E> extends Iterable<E> {/*** Returns a sequential Stream with this collection as its source * ...**/default Stream<E> stream() {return StreamSupport.stream(spliterator(), false);}}
Java 8增加支持了函数式编程(functional interface),就是只有一个抽象方法的接口,建议使用@FunctionalInterface Annotation来标记。Lambda表达式本身可以看作是一类functional interface,某种程度上这和面向对象算是两码事。我们熟知的Runnable、Callable等都是functional interface。
面向对象必须掌握的基本设计原则——S.O.L.I.D原则,这些原则更多应作为参考,并不是完全遵守这些原则,实践中要根据具体情况权衡。
- 单一职责(Single Responsibility),类或者对象最好是只有单一职责,某个类承担着多种职责(职责过重)可以考虑拆分。
- 开关原则(Open-Close, Open for extension, close for modification),设计要对扩展开放,对修改关闭。程序设计应保证平滑的扩展性,尽量避免因为新增同类功能去修改已有实现,可以少些回归(regression)问题。
- 里氏替换(Liskov Substitution),面向对象的基本要素之一,继承关系抽象时凡是可以用父类的地方,都可以用子类替换。
- 接口分离(Interface Segregation),设计类和接口时,如果一个接口定义了太多方法,子类很可能面临两难,只有部分方法对它有意义,破坏了内聚性。这种可以拆分多个接口,将行为进行解耦。某个接口的变化不会对使用其他接口的子类造成影响。
- 依赖反转(Dependency Inversion),实体应该依赖于抽象而不是实现。高层次模块不应该依赖于低层次模块,而是应该基于抽象,更好保证产品代码之间适当耦合度。
Java 10 中引入了本地方法类型推断和 var 类型,这种语法上的便利增强了程序对实现的依赖,但微小的类型泄漏却带来了便利的书写和更高的代码可读性。
// 按照里氏替换原则,我们应该这样定义变量, List<String> list = new ArrayList<>();// 使用 var 类型可以简化 var list = new ArrayList<String>();// list 实际会被推断为 ArrayList <String> ArrayList<String> list = new ArrayList<String>();
找个实际 case 遵循上面的原则改造代码实现,看看下面代码怎么优化更好些?
public class PushCenter {void servicePush(T extend Message msg>) {if (msg instanceof HuaweiPush) {// HUAWEI 通道// do somthing} else if(msg instanceof XiaomiPush) {// Xiaomi 通道// do somthing}// ...}
}
这个 case 中业务逻辑集中在一起,servicePush 方法较大,不利于测试维护;增加新的机型Push(比如给 Vivo 手机用户下行 push 消息)需要直接修改服务方法代码,违反开闭原则,可能会意外影响不相关的某个类型逻辑,灵活性和可扩展性较差;代码复用性低。
遵循开关原则和单一职责,把算法的定义和实现分离(听起来好像策略模式,但这里只是简单的分开,没有完全按照策略模式实现),改造如下,
public class PushCenter {private Map<Message.TYPE, ServiceProvider> providers;void servicePush(T extend Message msg) {providers.get(msg.getType()).service(msg);}
}interface ServiceProvider{void service(T extend Message msg) ;
}class XiaomiPushServiceProvider implements ServiceProvider{void service(T extend Message msg){// do somthing}
}class HuaweiPushServiceProvider implements ServiceProvider{void service(T extend Message msg) {// do something}
}
参考:
Java中的委托(Delegation)_javadelegate-CSDN博客
Have Fun