封装
概念
利用抽象数据类型将数据和基于数据的操作封装在一起,使其构成一个不可分割的独立实体。
数据被保护在抽象数据类型的内部,尽可能地隐藏内部的细节,只保留一些对外接口使之与外部发生联系。
用户无需知道对象内部的细节,但可以通过对象对外提供的接口来访问该对象。
优点:
- 减少耦合: 可以独立地开发、测试、优化、使用、理解和修改
- 减轻维护的负担: 可以更容易被程序员理解,并且在调试的时候可以不影响其他模块
- 有效地调节性能: 可以通过剖析确定哪些模块影响了系统的性能
- 提高软件的可重用性
降低了构建大型系统的风险: 即使整个系统不可用,但是这些独立的模块却有可能是可用的
示例
public class Person {
private String name;
private int gender;
private int age;
public String getName() {
return name;
}
public String getGender() {
return gender == 0 ? "man" : "woman";
}
public void work() {
if (18 <= age && age <= 50) {
System.out.println(name + " is working very hard!");
} else {
System.out.println(name + " can't work any more!");
}
}
}
示例 Person 类封装 name、gender、age 等属性,外界只能通过 get() 方法获取一个 Person 对象的 name 属性和 gender 属性,而无法获取 age 属性,但是 age 属性可以供 work() 方法使用。
注意到 gender 属性使用 int 数据类型进行存储,封装使得用户注意不到这种实现细节。
并且在需要修改 gender 属性使用的数据类型时,也可以在不影响客户端代码的情况下进行。Tips: 在真实开发中,我们一般会把这样的类成为实体类 (entity)
继承
概念
- 继承机制是面向对象程序设计不可缺少的关键概念,是实现软件可重用的根基,是提高软件系统的可扩展性与可维护性的主要途径。
- 所谓继承是指一个类的定义可以基于另外一个已经存在的类,即子类基于父类,从而实现父类代码的重用,子类能吸收已有类的数据属性和行为,并能扩展新的能力。
- 继承描述的是 is-a 的关系
优点
// 父类 基类
class Animal{
...
}
// 子类 衍生类
class Cat extends Animal {
...
}
// 子类 衍生类
class Dog extends Animal {
...
}
继承中类的初始化顺序
初始化顺序:
- 父类–静态变量/父类–静态初始化块
- 子类–静态变量/子类–静态初始化块
- 父类–变量/父类–初始化块
- 父类–构造器
- 子类–变量/子类–初始化块
- 子类–构造器
结论:
- 子类的静态变量和静态初始化块的初始化是在父类的变量、初始化块和构造器初始化之前就完成了;
- 静态变量、静态初始化块顺序取决于它们在类中出现的先后顺序
- 变量、初始化块初始化顺序取决于它们在类中出现的先后顺序
- 静态变量、静态初始化块只会初始化一次
示例代码:
public class TestExtends2 {
public static void main(String[] args) {
Son son = new Son();
System.out.println("<======================>");
Son son2 = new Son("f-a");
System.out.println("<======================>");
Son son3 = new Son();
}
}
// 父类,基类
class Father{
private String name;
public static int step = 1;
// 静态代码按定义顺序从上往下执行,所有这里先定义了 step
static {
System.out.println(Father.step++ + "-父类-静态变量初始化");
System.out.println(Father.step++ + "-父类-静态代码块初始化");
}
public int var = Father.step++;
// 初始化块
{
System.out.println(var + "-父类-成员变量初始化");
System.out.println(Father.step++ + "-父类-初始化块初始化");
}
public Father(){
System.out.println(Father.step++ + "-父类-无参构造方法初始化");
}
public Father(String name) {
this.name = name;
System.out.println(Father.step++ + "-父类-有参构造方法初始化");
}
}
// 子类、衍生类
class Son extends Father{
public static int sonStation = Father.step++;
static {
// 静态代码按定义顺序从上往下执行,所有这里先定义了 step
System.out.println(sonStation + "-子类-静态变量初始化");
System.out.println(Father.step++ + "-子类-静态代码块初始化");
}
public int var = Father.step++;
// 初始化块
{
System.out.println(var + "-子类-成员变量初始化");
System.out.println(Father.step++ + "-子类-初始化块初始化");
}
// 调用自己的无参构造方法
public Son() {
// 调用父类的无参构造方法,写或不写都会默认生成
// 如果父类没有无参构造方法,必须指明调用那个构造方法
// super();
System.out.println(Father.step++ + "-子类-无参构造方法初始化");
}
public Son(String name) {
// 在子类构造方法中的第一行语句,使用 super 调用父类构造方法
super(name); // 调用父类的有参构造方法
System.out.println(Father.step++ + "-子类-有参构造方法初始化");
}
/** 执行结果
* 1-父类-静态变量初始化
* 2-父类-静态代码块初始化
* 3-子类-静态变量初始化
* 4-子类-静态代码块初始化
* 5-父类-成员变量初始化
* 6-父类-初始化块初始化
* 7-父类-无参构造方法初始化
* 8-子类-成员变量初始化
* 9-子类-初始化块初始化
* 10-子类-无参构造方法初始化
* <======================>
* 11-父类-成员变量初始化
* 12-父类-初始化块初始化
* 13-父类-有参构造方法初始化
* 14-子类-成员变量初始化
* 15-子类-初始化块初始化
* 16-子类-有参构造方法初始化
* <======================>
* 17-父类-成员变量初始化
* 18-父类-初始化块初始化
* 19-父类-无参构造方法初始化
* 20-子类-成员变量初始化
* 21-子类-初始化块初始化
* 22-子类-无参构造方法初始化
*/
}
方法的重写(覆盖)
- 重写:如果父类的方法无法满足子类的使用,那么子类可以使用同名方法覆盖父类方法
- 要求:方法名、参数列表、返回名一致,访问修饰符不能比父类的权限小,抛出异常范围不能比父类大
- 规范:重写的方法用注解
@Override
标识缺点
- 继承是实现类复用的重要手段,但是继承会破坏封装。
- 每个类都应该封装其内部信息和实现细节,仅暴露必要的方法给其他类使用。但子类可以直接访问父 类的内部信息,甚至可以重写父类的方法,这增加了子类和父类的耦合度,破坏了封装。
设计父类时,应遵循以下原则:
- 尽量将父类的成员变量设置为
private
。 - 尽量缩小方法的访问权限,能私有尽量不要共有。
- 不希望被子类重写的方法设置为
final
。 -
多态
概念
多态分为编译时多态和运行时多态
编译时多态 ——主要指方法的重载,发生在本类
- 运行时多态——指程序中定义的对象引用所指向的具体类型在运行期间才确定,发生在 父类 和 子类之间(有继承关系的或者有接口关系的)
运行时多态有三个条件:
class Animal{
...
}
class Cat extends Animal {
...
}
class Dog extends Animal {
...
}
// 对象 c 可以调用 猫类的的所有方法,
Cat c = new Cat();
// 向上转型, Cat 对象提升到 Animal 对象
// a的引用指向 Cat 的对象,a只能调用动物类(Animal)中定义的方法,调用猫类(Cat)扩展的方法会报错
Animal a = new Cat();
// 在集合中,我们会经常使用到这个方法
List list = new ArrayList(); // 这也是父类引用子类的常见使用
多态环境下的调用
- 对成员方法的调用——编译看左边,运行看右边
```java
class Animal{
void show() {
} }System.out.println("Animal");
class Cat extends Animal {
void show() {
System.out.println(“Cat”);
}
}
…
Animal a = new Cat();
a.show(); // 调用的是子类(Cat)中的方法
- **对静态方法的调用——编译和运行都看左边**
```java
class Animal{
static void show() {
System.out.println("Animal");
}
}
class Cat extends Animal {
static void show() {
System.out.println("Cat");
}
}
...
Animal a = new Cat();
a.show(); // 调用的父类(Animal)中静态成员方法
- 对成员变量法的调用——编译和运行都看左边 ```java class Animal{ int num = 3; }
class Cat extends Animal { int num = 4; } … Animal a = new Cat(); a.num; // 调用的父类(Animal)中成员变量
> 变量不存在被子类覆盖的说法,只有方法存在覆写
<a name="poQri"></a>
#### 多态参数
方法的形式参数类型是**父类类型**,而传递的实际参数可以是**任意子类对象**<br />**方法参数多态性的好处:提高代码扩展性**
```java
class Animal{
void eat() {}
}
class Cat extends Animal {
void eat() {}
}
class Dog extends Animal {
void eat() {}
}
// 方法的形式参数类型是父类类型,而传递的实际参数可以是任意子类对象
method(Animal animal) {
animal.eat();
}
instanceof 操作符
instanceof
是判断对象类型的,常用来判断一个对象类是否为一个特定对象的实例
// 格式:boolean result = 对象名称 instanceof 类型
class Person {
...
}
Person p = new Person();
boolean result = Person instanceof p;
抽象类
概念
- 如果一个类中没有包含足够的信息来描述一个具体的对象,这样的类就是抽象类
- 比如形状类就是抽象类,圆形,三角形就是具体的类
- 用
abstract
修饰的类就是 抽象类。 - 如果某个类中包含有抽象方法那么该类必须定义为 抽象类 ,但是抽象类不一定有抽象方法
- 抽象类可以有 成员属性 和 非抽象成员方法
- 抽象类不能被实例化,但是可以有构造函数
- 抽象类只能用作基类,表示的是一种继承关系。
- 继承抽象类的非抽象类必须实现其中的所有抽象方法,而且已实现方法的参数、返回值要和抽象类中的方法一样;否则,该类也必须声明为抽象类。
抽象类的定义
使用关键字 abstract
定义抽象类
[访问权限] abstract class 类名 {
成员列表
}
示例:
public abstract class Animal {
public abstract void eat();
}
注意事项
- 抽象类可以有构造方法,但不能直接实例化,只能用来继承;
- 抽象类的派生子类应该提供对其所有抽象方法的具体实现如果抽象类的派生子类没有实现其中的所有抽象方法,那么该派生子类仍然是抽象类,只能用于继承,而不能实例化;
- 抽象类中也可以包含有非抽象的方法,子类中重写非抽象方法时返回值和参数必须与父类一致;
- 构造方法和静态方法不可以修饰为
abstract
。
抽象方法
抽象方法的语法:
public abstract class Animal {
// [访问权限] abstract 返回值类型 方法名(参数列表);
public abstract void eat();
}
class Cat extends Animal{
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
抽象方法的相关概念
- 类无法提供具体的实现,可以将此方法声明成 抽象方法
- 在类中没有方法体的方法,就是抽象方法
- 含有抽象方法的类,一定是抽象类
抽象的作用
- 在面向对象领域,抽象类主要用来进行类型隐藏;也就是使用抽象的类型来编程,但是具体运行时就可以使用具体类型。
- 利用抽象的概念,能够在开发项目中创建扩展性很好的架构,优化程序。
- 抽象类,抽象方法,在软件开发过程中都是设计层面的概念。也就是说,设计人员会设计出抽象类,抽象方法,程序员都是来继承这些抽象类并覆盖抽象方法,实现具体功能。
接口
Java 中的继承都是一对一的,为了解决这个问题,Java 提出了接口的概念
注意:
- 接口中的变量默认是
public static final
的 -
接口的定义
Java 接口是一系列方法的声明,是一些抽象的集合
- 一个接口的抽象方法没有方法实现,因为这些方法可以在不同的地方被不同的类实现,而这些实现可以有不同的行为
- 接口就是特殊的抽象类,即所有的方法都是抽象方法的抽象类就是 Java 中的接口 (interface)
接口的格式
[修饰符] interface 接口名 [extends 父类名列表] {
[public] [static] [final] 常量 ;
[public] [abstract] 方法 ;
}
- 修饰符:可选,用于指定接口的访问权限,可选值为
public
。即使省略,也依然是public
。 - 接口名:必选参数,用于指定接口的名称,接口名必须是合法的Java
- 标识符。一般情况下,要求首字母大写。
extends
父接口名列表:可选参数,用于指定要定义的接口继承于哪个父接口。当使用extends
关键字时,父接口名为必选参数。父接口可以存在多个,用逗号隔开。方法:接口中的方法只有定义而没有被实现。java 8中可以定义
default
、static
具体方法接口的特点
一种特殊的抽象类,
abstract
可以省略- 接口中没有变量,只能由
public static final
修饰的静态常量。三个修饰符可以省略 - 接口中所有的方法都是抽象方法,默认都是 public 权限
public interface User {
public static final int TYPE = 1;
// 只有定义,没有实现 java 1.8中可以定义default、static方法体
public int addUser(String name,String password);
public User addUser(User user);
public void land();
}
接口的实现
接口的实现可以实现一对多
public class UserImpl implements User, [xxxxx,xxxx,...] {
// 这里实现了 User 接口,下面必须是方法的实现
public int addUser(String name,String password) {
xxxx
}
public User addUser(User user) {
xxx
}
public abstract void land() {
xxx
}
}
类实现接口的特点:
- 类实现接口,本质
- 上与类继承类相似,区别在于“类最多只能继承一个类,即单继承,而一个类却可以同时实现多”个接口多个接口用逗号隔开即可。实现类需要覆盖所有接口中的所有抽象方法,否则该类也必须声明为抽象类。
- 接口是抽象的,接口中没有任何具体方法和变量, 所以接口不能进行实例化。java 8中可以定义
default
、static
具体方法 接口定义的是多个类都要实现的操作,即“what to do”。类可以实现接口,从而覆盖接口中的”方法,实现“how to do’。
接口继承接口
Java 接口继承接口原则
- Java 接口可以继承多个接口
- 接口继承接口依然使用关键字
extends
,不要错用implements
- Java 接口继承接口形式
- interface3 extends interface1, interface2 …..
接口继承与类继承
- 接口继承与类继承对比: Java类的继承是单一-继承,Java接口的继承是多重继承。
接口可实现多继承原因分析
接口只有定义,不能有方法的实现,java 8中可以定义
default
、static
具体方法,而抽象类可以有抽象方法也可以具体方法。- 实现接口的关键字为
implements
,继承抽象类的关键字为extends
。 - 一个类可以实现多个接口,但一个类只能继承一个抽象类。所以,使用接口可以间接地实现多重继承。
- 接口强调特定功能的实现,而抽象类强调所属关系。
- 接口成员变量默认为
public static final
,必须赋初值,不能被修改;其所有的成员方法都是public
、abstract
的。 - 抽象类中成员变量默认
default
,可在子类中被重新定义,也可被重新赋值;抽象方法被abstract
修饰,不能被private
、static
、synchronized
和native
等修饰,必须以分号结尾,不带花括号。 接口被用于常用的功能,便于日后维护和添加删除,而抽象类更倾向于充当公共类的角色,不适用于日后重新对立面的代码修改。
面向接口编程
面向接口编程和面向对象编程并不是平级的,它并不是比面向对象编程更先进的一种独立的编程思想,而是附属于面向对象思想体系,属于其一部分。
- 或者说,它是面向对象编程体系中的思想精髓之一。
- 面向接口编程的意思是指在面向对象的系统中所有的类或者模块之间的交互是由接口完成的。
Java8 接口的改变
1.增加了default
方法和static
方法,这两种方法完全可以有方法体
2.default
方法属于实例,static
方法属于类(接口)
3.接口中的静态方法不会被继承,接口中的静态变量会被继承this & super
this
通常指代当前对象,super
通常指代父类。this 介绍
this
是自身的一个对象,代表对象本身,可以理解为:指向对象本身的一个指针。注意: this关键字必须放在非静态方法里面,因为静态方法和当前对象没有关系
this 的使用
this 的用法在 Java 中大体可以分为3种:
普通的直接引用
相当于是指向当前对象本身,可以用
**this.xxx**
来引用当前对象的成员形参与成员名字重名,用 this 来区分
_private int _age;<br />_public void _setAge(_int _age){<br /> _this_.age = age;<br />}
- 引用构造函数
this(参数):调用本类中另一种形式的构造函数(应该为构造函数中的第一条语句)。
super 介绍
super
可以理解为是指向自己超(父)类对象的一个指针,而这个超类指的是离自己最近的一个父类。
super 的使用
- 普通的直接引用
与 this
类似,super
相当于是指向当前对象的父类,这样就可以用 **super.xxx**
来引用父类的成员。
- 子类中的成员变量或方法与父类中的成员变量或方法同名
- 引用构造函数
super(参数):调用父类中的某一个构造函数(应该为构造函数中的第一条语句)。
this & super 使用示例
public class TestExtends {
public static void main(String[] args) {
Son son = new Son();
System.out.println("<======================>");
son = new Son("zhangSan");
System.out.println("<======================>");
son = new Son("liSi", 18);
System.out.println("<======================>");
son.setName("wangWu");
}
}
class Father {
private String name;
public void setName(String name){
this.name = name;
prt("形参与成员名字重名,用 this 来区分");
}
public static void prt(String s) {
System.out.println(s);
}
Father() {
prt("父类·无参数构造方法");
}
Father(String name) {
prt("父类·含一个参数的构造方法");
this.name = name;
prt("父类·形参 name 与成员名字 name 重名,用 this 来区分");
}
}
class Son extends Father {
public int age;
public String name;
@Override
public void setName(String name){
super.setName(name);
prt("子类·成员方法 setName 与父类中的方法 setName 同名, 用 super 区分");
this.name = name;
prt("子类·形参 name 与成员名字 name 重名,用 this 来区分");
}
Son() {
// 调用父类无参构造方法,写或不写都会默认添加
super();
prt("子类·调用父类-无参数构造方法");
}
Son(String name) {
// 调用父类具有相同形参的构造方法
super(name);
prt("子类·调用父类含一个参数的构造方法");
this.name = name;
prt("子类·形参 name 与成员名字 name 重名,用 this 来区分");
}
Son(String name, int age) {
// 本类另一个具有相同形参的构造方法
this(name);
prt("子类:调用子类具有相同形参的构造方法");
this.age = age;
prt("子类·形参 age 与成员名字 age 重名,用 this 来区分");
}
}
this & super 的异同
super(参数)
:调用基类中的某一个构造函数(应该为构造函数中的第一条语句)this(参数)
:调用本类中另一种形成的构造函数(应该为构造函数中的第一条语句)super
: 它引用当前对象的直接父类中的成员(用来访问直接父类中被隐藏的父类中成员数据或函数,基类与派生类中有相同成员定义时如:super.变量名
super.成员函数据名(实参
)this
:它代表当前对象名(在程序中易产生二义性之处,应使用 this 来指明当前对象;如果函数的形参与类中的成员数据同名,这时需用 this 来指明成员变量名)- 调用
super()
必须写在子类构造方法的第一行,否则编译不通过。 - 每个子类构造方法的第一条语句,都是隐含地调用 super(),如果父类没有这种形式的构造函数,那么在编译的时候就会报错。
super()
和this()
类似,区别是,super()
从子类中调用父类的构造方法,this()
在同一类内调用其它方法。super()
和this()
均需放在构造方法内第一行。- 尽管可以用
this
调用一个构造器,但却不能调用两个。 this
和super
不能同时出现在一个构造函数里面,因为this必然会调用其它的构造函数,其它的构造函数必然也会有super
语句的存在,所以在同一个构造函数里面有相同的语句,就失去了语句的意义,编译器也不会通过。this()
和super()
都指的是对象,所以,均不可以在static
环境中使用。包括:static
变量,static
方法,static
语句块。- 从本质上讲,
this
是一个指向本对象的指针, 然而super
是一个 Java 关键字。OOP 概念总结
OOP 的三大特征
- 封装
- 继承
-
封装 (安全性)
借助 private 修饰符去修饰属性,让属性私有化,保护属性的值的安全性
- 借助 public 修饰符的方法去为私有属性提供 属性值的设置和获取 => getter 和 setter
继承(复用性)
当分析发现了很多类出现了共同的属性和方法,而且使用频率很高,使用继承
- 把相同的方法和属性抽象到父类,让子类继承并获取属性和方法
- 方法的重写:当父类的方法无法满足子类的使用,子类能够加以改造
- 重写特点:重写发生在子父类关系中,名字相同,参数列表相同,返回值相同,访问权限修饰符可以不同
- 抽象方法:当父类中的方法无法诠释,就声明为抽象方法,必须被子类重写(除非子类是抽象类)
重载 与 重写
重载
- 发生同一个类中
- 名字相同
- 参数列表不完全相同
- 返回值相同
-
重写
发生在父子类中
- 名字相同
- 参数列表相同
- 返回值相同
-
多态 (增强程序的多样性)
多态的前提
有继承
- 有子类对象获取父类引用
- 方法必须重写
接口与抽象类
接口不是类,所以没有构造方法,属性全为常量,方法全为抽象方法
抽象类和接口的区别:
- 抽象类(类的模板)
- 有构造方法,不能实例化(给子类使用)
- 属性可以是变量,也可以是常量
- 单一继承
- 方法可以为抽象方法,可以为普通方法
- 接口(方法的模板)
- 不是类
- 所以没有构造方法
- 属性全为常量
- 方法均为抽象方法
- 一个类可以使用多个接口
五大基本原则
- 单一职责原则(Single-Resposibility Principle)
一个类,最好只做一件事,只有一个引起它的变化。单一职责原则可以看做是低耦合、高内聚在面向对象原则上的引申,将职责定义为引起变化的原因,以提高内聚性来减少引起变化的原因。
- 开放封闭原则(Open-Closed principle)
软件实体应该是可扩展的,而不可修改的。也就是,对扩展开放,对修改封闭的。
- 里氏替换原则(Liskov-Substituion Principle)
子类必须能够替换其基类。这一思想体现为对继承机制的约束规范,只有子类能够替换基类时,才能保证系统在运行期内识别子类,这是保证继承复用的基础。
- 依赖倒置原则(Dependecy-Inversion Principle)
依赖于抽象。具体而言就是高层模块不依赖于底层模块,二者都同依赖于抽象;抽象不依赖于具体,具体依赖于抽象。
- 接口隔离原则(Interface-Segregation Principle)
使用多个小的专门的接口,而不要使用一个大的总接口