Java面向对象编程
一、面向对象的三大特性
-
封装
其核心思想就是隐藏不需要对外公开的属性和方法,以减少对外耦合,包证数据的安全和程序的稳定。例如:通常使用private修饰成员变量,提供对应的getter、setter方法,外部只能通过方法来控制成员变量;同时把代码用方法进行封装,提高代码的复用性。
Java访问权限关键字:
本类内部 本包 子类 外部包 public ✓ ✓ ✓ ✓ protected ✓ ✓ ✓ × default ✓ ✓ × × private ✓ × × × 说明:Java访问权限控制是在编译层,不会再类文件中留下痕迹。通过反射机制,还是可以访问类的私有成员。
-
继承
多个类共同的成员变量和成员方法,抽取到另外一个类中(父类),再让多个类去继承这个父类。
特点:
-
子类继承父类的成员变量
当子类继承了某个类之后,便可以调用父类中的成员变量,但是并不是完全继承父类的所有成员变量。规则如下:
1) 能够访问父类的public和protected成员变量;不能访问父类的private成员变量(只是拥有,不能使用,可以通过反射获取);
2) 对于父类的包访问权限成员变量,如果子类和父类在同一个包下,则子类能继承;否则,不能继承;
3) 对于子类可以继承的父类成员变量,如果在子类中出现了同名称的成员变量,则会发生隐藏现象,即子类的成员变量会屏蔽掉父类的同名成员变量。如果要在子类中访问父类中同名的成员变量,需要使用super关键字来进行引用。
-
子类继承父类的方法
1) 能够访问父类的public和protected成员方法;不能访问父类的private成员方法(只是拥有,不能访问,可以通过反射访问);
2) 对于父类的包访问权限的成员方法,如果子类和父类在同一个包下,则子类能够继承;否则,子类不能继承;
3) 对于子类可以继承的父类的成员方法,如果在子类中出现了同名称和参数的成员方法,则是重写,即子类的成员方法会覆盖父类的成员方法(父类的成员方法如果是静态的,子类也必须的静态的才能覆盖,不然编译错误)。如果要在子类中访问父类中同名的成员方法,需要使用super关键字来进行引用。
public class Parent { private String name; private void setName(String name) { this.name = name; } private String getName() { return "hello!" + this.name; } } public class Child extends Parent { } public class Client { public static void main(String[] args) throws Exception { Child child = new Child(); Class<?> clazz = child.getClass(); Class<?> superClazz = clazz.getSuperclass(); Method superMethod = superClazz.getDeclaredMethod("setName", String.class); // 取消访问检查 superMethod.setAccessible(true); superMethod.invoke(child, "张三"); superMethod = superClazz.getDeclaredMethod("getName"); superMethod.setAccessible(true); Object returnValue = superMethod.invoke(child); System.out.println(returnValue); Field[] fields = superClazz.getDeclaredFields(); for (Field field : fields) { System.out.println(field.getName()); } } } 输出结果: hello!张三 name
-
构造器
子类不能继承父类的构造器,但是如果父类有带参构造器,并且没有默认构造器,那么子类必须有同类型参数构造,如果父类是无参构造,那么子类任意构造都可以。
-
-
多态
父类的引用指向子类对象:父类类型 引用名称 = new 子类类型
-
多态成员变量
当子类中出现同名的成员变量时,多态调用该变量时:
编译时期:参考的是引用型变量所属的类中是否有被调用的成员变量。没有,编译失败。
运行时期:也是调用引用型变量所属的类中的成员变量。
也就是编译和运行都参考等号的左边
public class Parent { public String name = "parent"; } public class Child extends Parent { public String name = "child"; } public class Test { public static void main(String[] args){ Parent child = new Child(); System.out.println(child.name); } } 输出结果: parent
-
多态成员方法
编译时期:参考引用变量所属的类,如果类中没有调用的方法。编译失败。
运行时期:参考引用变量所指的对象所属的类,并运行对象所属类中的成员方法。
也就是编译看左边(父类是否拥有该方法),运行看右边(静态都看左边)
public class Parent { public void hello() { System.out.println("hello!parent"); } public static void hi() { System.out.println("hi!parent"); } } public class Child extends Parent { @Override public void hello() { System.out.println("hello!child"); } public static void hi() { System.out.println("hi!child"); } } public class Test { public static void main(String[] args){ Parent child = new Child(); child.hello(); child.hi(); } } 输出结果: hello!child hi!parent
-
二、对象的分类
-
类(抽象类)
如果一个类含有抽象方法,则这个类一定是抽象类(不包含抽象方法,但被abstract修饰的也是抽象类),抽象类必须在类前使用abstract关键字修饰。因为抽象类中含有无具体实现的方法,所以不能用抽象类创建对象。
抽象类和普通类的主要区别:
1)抽象方法必须为public或者protected(如果为private,则不能被子类继承,子类便无法实现该方法),缺省情况下默认为public。
2)抽象类有构造方法,但不能用来创建对象。
3)如果一个类继承于一个抽象类,则子类必须实现父类的抽象方法。如果子类没有实现父类的抽象方法,则必须将子类也定义为抽象类。
-
内部类
为什么需要内部类:无论该内部类的外部类实现或者继承了什么都不会对它造成影响。
1)成员内部类,定义在外部类中的成员位置。与类中的成员变量相似,可通过外部类对象进行访问
public class Outer { class Inner { public void hi(String name) { System.out.println("hi!" + name); } } static class Inner2 { public void say(String name) { System.out.println("hello!" + name); } } public static void main(String[] args){ // 非静态情况 Outer.Inner inner = new Outer().new Inner(); inner.hi("李四"); // 静态 Outer.Inner2 inner2 = new Outer.Inner2(); inner2.say("王五"); } } 输出结果: hi!李四 hello!王五
2)局部内部类,定义在外部方法的局部位置。与访问方法中的局部变量相似,可以通过调用方法进行方法
public class Outer { public void say(String name) { class Inner { public void hi(String name) { System.out.println("hi!" + name); } } new Inner().hi(name); } public static void main(String[] args){ Outer outer = new Outer(); outer.say("赵六"); } } 输出结果: hi!赵六
3)匿名内部类,是创建某个类型子类对象的快捷方法
new Thread(new Runnable() { @Override public void run() { System.out.println("...."); } });
-
接口
接口中可以含有变量和方法。但是,接口中的变量会被隐式地指定为public static final变量(并且只能是public static final变量,用private修饰编译会报错),而方法会被隐式地指定为public abstract方法且只能是public abstract方法(用其他关键字,比如private、protected、static、final等修饰编译会报错),并且接口中的所有方法不能有具体的实现(除default方法,java8中接口可以有default方法),也就是说,接口中的方法必须都是抽象方法或default方法。可以看出接口是一种极度抽象的类型,它比抽象类更“抽象”,并且一般情况下不在接口中定义常量。
接口与抽象类的区别:
-
语法层面上的区别
1)抽象类可以提供成员方法的实现细节,而接口中只能存在抽象方法或默认方法;
2)抽象类中的成员变量可以是各种类型的,而接口中只能存在常量;
3)接口中不能含有静态代码块、构造代码块以及普通方法,而抽象类可以有静态代码块、构造代码块和静态方法;
4)一个类只能继承一个抽象类,而一个类可以实现多个接口
-
设计层面上的区别
1)抽象类是对一种事物的抽象,包括属性和行为,但是接口却仅仅是对局部(行为)进行抽象。
2)设计层面不同,抽象类作为很多子类的父类,它是一种模板设计。而接口是一种行为规范,更多的是表述不同的行为。
-
三、类的实例化过程
类的实例化过程如下:
-
JVM读取指定classpath路径下的class文件,加载到内存,如果有直接父类,也会加载父类;
-
堆内存分配空间;
-
执行父类、子类静态代码块;
-
对象属性进行默认初始化;
-
调用构造方法;
-
在构造方法中,先调用父类构造方法初始化父类数据;
-
初始化父类数据后,显示初始化,执行子类的构造代码块;
-
再进行子类构造方法的特定初始化;
-
初始化完毕后,将地址赋值给引用。
class Parent { int num = 5; static { System.out.println("父类静态代码块"); System.out.println(); } { System.out.println("父类构造代码块1," + num); num = 1; System.out.println("父类构造代码块2," + num); doSomething(); System.out.println(); } Parent() { System.out.println("父类构造方法1," + num); num = 2; System.out.println("父类构造方法2," + num); doSomething(); System.out.println(); } void doSomething() { System.out.println("父类doSomething方法1," + num); num = 3; System.out.println("父类doSomething方法2," + num); System.out.println(); } } class Child extends Parent { int num = 10; /* 静态代码块,在类加载时执行 */ static { System.out.println("子类静态代码块"); System.out.println(); } /* 构造代码块 */ { System.out.println("子类构造代码块1," + num); num = 11; System.out.println("子类构造代码块2," + num); doSomething(); System.out.println(); } Child() { System.out.println("子类构造方法1," + num); num = 12; System.out.println("子类构造方法2," + num); doSomething(); System.out.println(); } @Override void doSomething() { System.out.println("子类doSomething方法1," + num); num = 13; System.out.println("子类doSomething方法2," + num); System.out.println(); } } public class Test { public static void main(String[] args) { Child child = new Child(); child.num = 20; child.doSomething(); } } 输出结果: 父类静态代码块 子类静态代码块 父类构造代码块1,5 父类构造代码块2,1 子类doSomething方法1,0 子类doSomething方法2,13 父类构造方法1,1 父类构造方法2,2 子类doSomething方法1,13 子类doSomething方法2,13 子类构造代码块1,10 子类构造代码块2,11 子类doSomething方法1,11 子类doSomething方法2,13 子类构造方法1,13 子类构造方法2,12 子类doSomething方法1,12 子类doSomething方法2,13 子类doSomething方法1,20 子类doSomething方法2,13
四、面向对象的设计原则
-
单一职责原则
单一职责原则的定义是就一个类而言,应该仅有一个引起他变化的原因。也就是说一个类应该只负责一件事情。如果一个类负责了方法M1,方法M2两个不同的事情,当M1方法发生变化的时候,我们需要修改这个类的M1方法,但是这个时候就有可能导致M2方法不能工作。这个不是我们期待的,但是由于这种设计却很有可能发生。所以这个时候,我们需要把M1方法,M2方法单独分离成两个类。让每个类只专心处理自己的方法。
单一职责原则的好处如下:
可以降低类的复杂度,一个类只负责一项职责,这样逻辑也简单很多 提高类的可读性,和系统的维护性,因为不会有其他奇怪的方法来干扰我们理解这个类的含义 当发生变化的时候,能将变化的影响降到最小,因为只会在这个类中做出修改。
-
开闭原则
开闭原则和单一职责原则一样,是非常基础而且一般是常识的原则。开闭原则的定义是软件中的对象(类,模块,函数等)应该对于扩展是开放的,但是对于修改是关闭的。
当需求发生改变的时候,我们需要对代码进行修改,这个时候我们应该尽量去扩展原来的代码,而不是去修改原来的代码,因为这样可能会引起更多的问题。
这个准则和单一职责原则一样,是一个大家都这样去认为但是又没规定具体该如何去做的一种原则。
开闭原则我们可以用一种方式来确保他,我们用抽象去构建框架,用实现扩展细节。这样当发生修改的时候,我们就直接用抽象派生的一个具体类去实现修改。
-
里式替换原则
里氏替换原则是一个非常有用的一个概念。他的定义如下:
如果对每一个类型为T1的对象o1,都有类型为T2的对象o2,使得以T1定义的所有程序P在所有对象o1都替换成o2的时候,程序P的行为都没有发生变化,那么类型T2是类型T1的子类型。
简单的定义如下:
所有引用基类的地方必须能够透明地使用其子类的对象。
里氏替换原则通俗的去讲就是:子类可以去扩展父类的功能,但是不能改变父类原有的功能。他包含以下几层意思:
- 子类可以实现父类的抽象方法,但是不能覆盖父类的非抽象方法。
- 子类可以增加自己独有的方法。
- 当子类的方法重载父类的方法时候,方法的形参要比父类的方法的输入参数更加宽松。
- 当子类的方法实现父类的抽象方法时,方法的返回值要比父类更严格。
里氏替换原则之所以这样要求是因为继承有很多缺点,他虽然是复用代码的一种方法,但同时继承在一定程度上违反了封装。父类的属性和方法对子类都是透明的,子类可以随意修改父类的成员。这也导致了,如果需求变更,子类对父类的方法进行一些复写的时候,其他的子类无法正常工作。所以里氏替换法则被提出来。
确保程序遵循里氏替换原则可以要求我们的程序建立抽象,通过抽象去建立规范,然后用实现去扩展细节,这个是不是很耳熟,对,里氏替换原则和开闭原则往往是相互依存的。
-
依赖倒置原则
依赖倒置原则指的是一种特殊的解耦方式,使得高层次的模块不应该依赖于低层次的模块的实现细节的目的,依赖模块被颠倒了。 这也是一个让人难懂的定义,他可以简单来说就是:
高层模块不应该依赖底层模块,两者都应该依赖其抽象 抽象不应该依赖细节 细节应该依赖抽象
在Java 中抽象指的是接口或者抽象类,两者皆不能实例化。而细节就是实现类,也就是实现了接口或者继承了抽象类的类。他是可以被实例化的。高层模块指的是调用端,底层模块是具体的实现类。在Java中,依赖倒置原则是指模块间的依赖是通过抽象来发生的,实现类之间不发生直接的依赖关系,其依赖关系是通过接口是来实现的。这就是俗称的面向接口编程。
-
接口隔离原则
接口隔离原则的定义是:
客户端不应该依赖他不需要的接口
换一种说法就是类间的依赖关系应该建立在最小的接口上。这样说好像更难懂。我们通过一个例子来说明。我们知道在Java中一个具体类实现了一个接口,那必然就要实现接口中的所有方法。如果我们有一个类A和类B通过接口I来依赖,类B是对类A依赖的实现,这个接口I有5个方法。但是类A与类B只通过方法1,2,3依赖,然后类C与类D通过接口I来依赖,类D是对类C依赖的实现但是他们却是通过方法1,4,5依赖。那么是必在实现接口的时候,类B就要有实现他不需要的方法4和方法5 而类D就要实现他不需要的方法2,和方法3。这简直就是一个灾难的设计。
所以我们需要对接口进行拆分,就是把接口分成满足依赖关系的最小接口,类B与类D不需要去实现与他们无关接口方法。比如在这个例子中,我们可以把接口拆成3个,第一个是仅仅由方法1的接口,第二个接口是包含2,3方法的,第三个接口是包含4,5方法的。 这样,我们的设计就满足了接口隔离原则。
以上这些设计思想用英文的第一个字母可以组成SOLID ,满足这个5个原则的程序也被称为满足了SOLID准则。
-
迪米特原则
迪米特原则也被称为最小知识原则,他的定义是:
一个对象应该对其他对象保持最小的了解。
因为类与类之间的关系越密切,耦合度越大,当一个类发生改变时,对另一个类的影响也越大,所以这也是我们提倡的软件编程的总的原则:低耦合,高内聚。 迪米特法则还有一个更简单的定义:
只与直接的朋友通信。首先来解释一下什么是直接的朋友:每个对象都会与其他对象有耦合关系,只要两个对象之间有耦合关系,我们就说这两个对象之间是朋友关系。耦合的方式很多,依赖、关联、组合、聚合等。其中,我们称出现在成员变量、方法参数、方法返回值中的类为直接的朋友,而出现在局部变量中的类则不是直接的朋友。也就是说,陌生的类最好不要作为局部变量的形式出现在类的内部。
这里我们可以用一个现实生活中的例子来讲解一下。比如我们需要一张CD,我们可能去音像店去问老板有没有我们需要的那张CD,老板说现在没有,等有的时候你们来拿就行了。在这里我们不需要关心老板是从哪里,怎么获得的那张CD,我们只和老板(直接朋友)沟通,至于老板从他的朋友那里通过何种条件得到的CD,我们不关心,我们不和老板的朋友(陌生人)进行通信,这个就是迪米特的一个应用。说白了,就是一种中介的方式。我们通过老板这个中介来和真正提供CD的人发生联系。