面向对象和面向过程
- 面向过程(Procedure Oriented):把问题分解成一个个步骤,按照步骤调用函数。
- 自上而下,先定好函数入口,再逐步实现用到的方法
- 一个类具备各种各样的功能。
- 代码重用性低、扩展能力差,后期维护困难,代码耦合高。
- 面向对象(Object Oriented):把属性、行为封装成对象,对同类对象提取共性,形成类,通过不同对象之间的组合、调用解决问题。
- 自下而上,先设计组件,再通过对象间组合、调用,完成业务逻辑。
- 每个类只负责自己的属性和功能。
- 性能比面向过程低
概念解释
- 声明:告诉编译器有一个变量,不分配存储空间。例如
User user;
。先声明后赋值,即懒加载。 - 定义:给变量分配存储空间或赋值。例如
User user = new User();
- 创建:
new 类名()
的动作即为创建 - 实例化:创建一个对象也叫实例化一个对象。这个对象就是类的一个实例。
- 初始化:
- 对象初始化:创建对象的时候会进行初始化动作,给成员变量赋默认值,执行构造方法,代码块等
- 变量初始化:即给变量赋值
- 变量:变量分为基本类型变量和类变量,其中类变量也可以叫做引用
- 引用:
- 做名词时(也叫别名):是一个存放对象地址的变量,一个对象可以有多个变量引用。如user变量也叫user对象的引用
- 做动词时:指让变量指向堆空间的对象地址,简单理解为给变量赋引用类型的值(非基本类型)的过程。例如:给对象添加了xxx引用,一个变量引用了xxx对象
- 类的对象(Object):也叫类的实例(实例化对象,Instance),对象的类型是该类。对象创建之后存放在堆空间中。
- new创建
- 反射获得Class对象,调用newInstance创建
- 类对象(字节码对象):类加载的时候会为该类创建一个对象,是Class类型,同一个类在一个jvm中只存在一份字节码对象。
- Class.forName("包名")
- new Object().getClass()
- 类名.class
class Person {//Person是类
//属性、成员变量
private String name;
//构造方法
public Person(String name) {
this.name = name;
}
//方法
public void say(String str){
System.out.println(str);
}
}
//new用来在堆上创建对象,这里的“=”并不是赋值的意思,而是把对象的地址传递给变量
Person person = new Person("张三"); //person是引用(别名),new Person()是真正的对象,没有名字
//引用可以指向不同的对象
person = new Person("李四");
//对象可以有多个引用
Person person1;
Person person2;
person1 = new Person("张三");
person2 = person1;
三大特性
封装、继承、多态
- 封装:对客观事物进行抽象。将对象的属性和行为封装成一个类,并可以设置属性和方法对外的可见性。
- 类描述了一类事物的状态和行为,是一种抽象的数据类型。如:把人抽象成一个类,有四肢、五官,可以说话、走路
- 类是对象的抽象,对象是类的具体(实例)。类可以看作是对象的模板,一个类可以有多个对象。如:人是一个类,一个人是具体对象
- 继承:可以实现现有类的所有功能,并可以对现有类的功能进行扩展。
- 继承的类称为子类或派生类,被继承的类称为基类、父类、超类。
- 父类具有子类的共性,子类拥有父类没有的特性。如:把人当作基类,男人、女人可以作为子类,二者具备不同的特性。
- 子类中可以通过super使用父类方法
- 多态:同一个行为在不同情形下可以有不同的表现形式。
- 方法多态:重写和重载。
- 对象多态:父类和子类对象间的转化
- 向上转型:子类对象变为父类对象。父类 变量名称 = 子类实例。(自动转换)
- 向下转型:父类对象变为子类对象。子类 变量名称 = (子类) 父类实例。(需要强制转换)
- 参数多态:类定义和方法定义时不指定具体参数类型,把类型作为参数使用,创建实例或调用方法的时候可以指定不同的具体类型。如Java泛型(也叫参数化类型)
注:对方法的覆盖才有多态,对成员变量的覆盖没有多态。构造方法也没有多态,这很容易理解,因为无法重写构造方法。
public class Main {
static class A {
String a = "A";
String getA() {
return a;
}
}
static class B extends A {
String a = "B";
String getA() {
return a;
}
}
public static void main(String[] args) {
A obj = new B();
System.out.println(obj.a); //输出A,成员变量的覆盖没有多态
System.out.println(obj.getA()); //输出B,方法的覆盖才有多态
}
}
重写和重载
重载(Overload) | 重写(Override) | |
---|---|---|
定义 | 同一个类中,同一个方法名称,根据不同参数列表,可以完成不同的功能。 | 父类和子类具有同一个方法,通过操作不同子类对象,可以完成不同的功能。 |
要求 | 相同方法名,参数列表不同。即方法签名不同。 | 具有相同的方法名,参数,返回类型。即方法签名相同 |
时期 | 编译时多态:编译时根据方法签名(方法名和参数列表)确定调用的方法 | 运行时多态:运行时根据变量指向的实际对象确定真正调用的方法 |
访问修饰符 | 可以改变访问修饰符 | 重写方法访问权限不能比父类严格,但是可以比父类更松 |
抛出异常 | 可以抛出新的或更广的异常 | 重写方法抛出异常范围不能比父类大(RuntimeException除外),但是可以比父类异常范围更小或更少 |
发生类 | 可以发生在一个类中,也可以发生在父类和子类中 | 发生在父类和子类中,要求有类继承或接口实现 |
备注 | 只有返回值相同不能重载。 | final方法无法重写 |
每一个变量都有两种类型,ClassA obj = new ClassB();
- 静态类型(编译时类型、引用类型、声明类型):引用变量的类型,在编译期确定,无法改变。即
ClassA
- 动态类型(运行时类型、实际类型、真实类型):实例对象的类型,在编译期无法确定,需要运行期确定,可以改变。即
ClassB
继承与实现
继承(Inheritance) | 实现(Implement) | |
---|---|---|
定义 | 如果多个类的某个部分的功能相同,那么可以抽象出一个类,把他们的相同部分都放到父类里,让他们都继承这个类。 | 如果多个类处理的目标是一样的,但是处理的方法方式不同,那么就定义一个接口,让不同的类根据各自的具体逻辑实现这个接口。 |
目的 | 继承父类功能,复用代码。父类有具体实现 | 对多个类都具备的特定的行为定义一个公共标准。不包含具体实现 |
举例 | 鸟类会飞,相应的继承鸟类的麻雀、燕子等,也具备飞的功能 | 飞机和鸟都有飞的行为,但是飞的方式不同,具有不同的实现 |
关键字 | 使用extends | 使用implementation |
备注 | 抽象类可以定义抽象方法,抽象方法强制要求子类实现,非抽象方法不强制要求 | 接口方法默认是抽象的,强制要求子类实现。Java8之后接口可以有默认实现,使用default修饰 |
基类可以定义自己的属性、方法等 | 接口的属性和方法只能是public的。属性只能是全局常量(static final),方法不能用final修饰 | |
一个类只能继承一个类 | 一个类可以实现多个接口 |
补充:
- 继承和实现都是描述两个类间的关系,继承关系比实现关系更加紧密:只要两个类有一定的相似性就可以抽取接口,而继承要求子类和父类有大部分共性,子类是由父类衍生出来的。
- 继承和实现并不完全对应类和接口。继承关系中,子类也可以有实现。Java8中,接口可以有默认实现,子类可以继承默认实现
- 一个接口可以继承多个父接口(不需要实现)。A extends B,C
- 子类属性会覆盖父类的同名属性。
多继承带来的问题?
菱形问题:如图,D类继承B类和C类,B类和C类又同时继承A类。D会因为多继承,继承到两份A类的属性和方法。
C++引入了虚继承解决菱形问题。Java类不允许多继承,但是接口支持多继承,并且Java8中接口也可以有默认实现。
如果B和C接口有同名方法,且都有默认实现,则D类需要重写方法。
如果A和B接口有同名方法,且都有默认实现,则使用B接口的默认实现,相当于B接口重写了方法
问题
public class Main {
public void test() {
Object s = null;
foo(s); //编译错误,大类型无法转为小类型,需要强制转换
foo(null); //编译错误,无法确定调用哪个重载方法
}
private void foo(String s) {}
private void foo(Integer s) {}
}
public class Main {
public void test() {
foo(1); //调用double方法。此时不会自动装箱,会进行自动类型转换
}
private void foo(double s) {}
private void foo(Integer s) {}
}
接口和抽象类
接口作用,如何理解面向接口编程?
- 对业务逻辑进行抽象
- 实现多态
- 对外部隐藏细节,不涉及具体实现细节
接口和抽象类都不能直接实例化,需要子类继承和实现。
参数 | 抽象类 | 接口 |
---|---|---|
抽象层次 | 对类的抽象 | 对行为的抽象 |
默认的方法实现 | 可以定义抽象方法和普通方法,抽象方法强制要求子类实现,非抽象方法不强制要求 | 接口方法默认是完全抽象的,强制要求子类实现。java8可以使用接口方法的默认实现,用default修饰 |
属性和方法 | 可以定义自己的属性、方法等 | 接口的属性和方法只能是public的。属性只能是全局常量(public static final),方法不能用final修饰 |
使用 | 子类使用extends关键字来继承抽象类。如果子类不是抽象类的话,需要实现所有的抽象方法。 | 子类使用implements关键字来实现接口。如果子类不是抽象类的话,需要实现所有接口方法 |
构造器 | 抽象类可以有构造器 | 接口不能有构造器 |
访问修饰符 | 抽象方法可以有public、protected和default这些修饰符 | 接口方法默认修饰符是public。不可以使用修饰符。 |
main方法 | 抽象方法可以有main方法并且我们可以运行它 | 接口没有main方法,因此我们不能运行它。(java8以后接口可以有default和static方法,所以可以运行main方法) |
多继承 | 可以继承其他抽象类、实现其他接口 | 只可以继承其他接口 |
子类多继承 | 抽象类不能被多继承 | 接口可以被多实现,注意避免多个接口定义同名方法 |
添加新方法 | 往抽象类中添加新的方法,可以提供默认的实现。不需要修改子类 | 往接口中添加方法,需要修改实现类 |
设计层次 | 抽象类是自底向下抽象而来的 | 接口是自顶向下设计来的 |
继承与组合
具体可见设计模式-面向对象基本原则中的合成复用原则
继承 | 组合 |
---|---|
强调对象是什么的关系。即is-a |
强调整体与部分拥有的关系,即has-a |
不支持动态继承:编译时就确定好两个类的继承关系,运行时无法替换父类 | 支持动态组合:可以在运行时选择不同类型的组合对象 |
自动继承父类的功能 | 不能自动继承父类的功能,需要手动包装方法 |
创建子类对象的时候,无须创建父类对象 | 创建整体类对象时,需要先创建局部类对象并传入 |
多用组合,少用继承
- 继承和组合都体现了类的复用性
- 组合比继承耦合度低,更灵活,更容易扩展
- 两个类确实存在
is-a
关系的时候才使用继承
类的定义
访问修饰符
Java中有四种访问修饰符,可以修饰类、属性、方法,默认访问修饰符是default。在接口中默认是public,default表示接口默认实现
- public:所有类和对象都可访问
- protected:同一个包中的类,以及其他包的子类可访问
- default:同一个包中的类可以访问,其他包的类不可访问,即使是子类
- private:除了当前类可以访问,其他类都不能访问,包括子类
变量
- 类变量(静态变量):存储在JVM方法区中,类加载的时候初始化
- 成员变量:存储在JVM堆内存中,有默认初始值,创建对象的时候初始化
- 局部变量:存储在JVM栈内存中,没有默认初始值,使用之前需要赋值
构造函数(构造方法)
构造函数是特殊的方法,用来创建并初始化对象。
特点:
- 没有返回类型
- 不能被重写
- 名称和所属类的名称相同
- 没有声明构造函数的情况下,会生成默认无参构造函数。如果手动定义了一个有参数的构造函数,不会再生成默认无参构造函数
- 继承的时候,子类实例化会先调用父类的构造方法,因此子类的构造方法中一定要调用super。如果父类是无参构造函数,编译器会隐式帮我们调用super。
- 继承的时候,如果父类声明了有参构造函数,子类一定要定义一个构造函数(可以是有参,也可以是无参),并且需要手动调用父类构造函数,即
super(参数列表)
:不定义的话子类会生成默认无参构造函数,并隐式调用父类无参构造函数,即super()
。而父类此时并没有无参构造函数,因此会编译失败。
this和super
this和super使用方式:
- 通过this引用自身属性和方法。通过super引用父类属性和方法。
- this和super无法在静态环境中使用:包括静态方法、静态变量、静态内部类、静态代码块等
- this一般可以缺省,除非有同名的局部变量,无法指代的时候。
- 在内部类中(非静态内部类、匿名内部类),需要使用
类名.this.属性/方法
、类名.super.属性/方法
进行引用 - 构造方法引用不需要
.
,如this(参数列表),super(参数列表)
- 不能在普通方法中使用this和super调用构造方法,只能在其他构造方法中。
- 使用this和super调用构造方法的时候,必须放在构造函数的第一行
- 在一个构造方法中不能同时调用this和super构造方法:不能调用两次this,不能调用两次super,也不能一次this一次super。
- 子类构造方法中会隐式的调用父类无参构造函数,即
super()
。如果父类没有默认无参构造函数,编译器会报错,需要使用super手动调用有参构造函数。
this和super的主要区别:
子类重写并不会删除父类的方法,只是对子类隐藏了父类的方法,通过super来访问隐藏的父类方法和成员变量。
子类实例化的时候会调用父类的构造函数,但是并不会创建父类对象,而是借用父类的构造方法来创建子类对象。会从父类中继承成员变量并分配空间,但不会给父类对象划分空间。
this指代当前实例对象的引用,super是java提供的关键字,不是父类的实例的引用。
如何证明super不是父类对象的引用? >
- 不能将super赋值给变量,但是可以将this赋值给变量
- 抽象类不能创建对象,但父类是抽象类的时候,仍然可以通过super调用父类方法
super.test()
是调用父类中的test方法,不是说调用父类对象的test方法
子类中的this很好理解,毫无疑问是自身。那么父类中的this指代的是什么?是实际的类型,还是父类自身?
根据上面的结论,实例化子类的时候并不会创建父类对象,因此this不可能是父类对象。那么this永远引用的是子类的元素吗?
- this获取对象引用,指代的是实际对象。
- this.方法:由于重写方法多态,访问的是子类的方法
- this.成员变量:由于成员变量没有多态,因此访问的是父类的变量
- this构造方法:由于构造方法没有多态,访问的是父类的其他构造方法
- this作为参数:此时this的静态类型是父类自身,无法确定实际类型。由于方法重载是编译时的多态,因此根据静态类型进行分派。
总结:this相当于变量,this的静态类型为父类,实际类型为子类。静态分派取决于静态类型,动态分派取决于实际类型
public class Main {
static class A {
String a = "A";
String getA() {
return a;
}
void test() {
A obj = this; //不需要强转
B obj2 = (B) this; //需要强转,说明this的声明类型是A
System.out.println(obj); //输出B对象,相当于调用obj.toString(),方法有多态
System.out.println(this); //输出B对象,相当于调用this.toString(),方法有多态
System.out.println(this.a); //输出A,成员变量没有多态
System.out.println(this.getA()); //输出B,方法有多态
this.print(this); //输出:B printA:B对象。
//重写是运行时的多态,因此调用的是B类的print方法
//重载是编译时的多态,根据静态类型进行分派,此时this的静态类型是A,实际类型无法确定,因此选择的是参数为A的方法
}
void print(A obj) {
System.out.println("A printA:" + obj);
}
void print(B obj) { //重载
System.out.println("A printB:" + obj);
}
}
static class B extends A {
String a = "B";
String getA() { //重写
return a;
}
void print(A obj) { //重写
System.out.println("B printA:" + obj);
}
void print(B obj) {
System.out.println("B printB:" + obj);
}
}
public static void main(String[] args) {
A obj = new B();
System.out.println(obj.a); //输出A,成员变量没有多态
System.out.println(obj.getA()); //输出B,方法有多态
obj.test();
}
}
instanceof和isInstance
instanceof
是关键字,用于判断对象是否属于某个类型isInstance
是Class类的方法,用于判断对象能否强转为该类型,内部调用isAssignableFrom
实现isAssignableFrom
是Class类的方法,用于判断一个类A是否是另一个类B的超类或接口。A.class.isAssignableFrom(B.class)
用法:
Object obj = "abc";
System.out.println(obj instanceof String);//true
System.out.println(obj instanceof Object);//true
System.out.println(String.class.isInstance(obj));//true
System.out.println(Object.class.isInstance(String.class));//true
//对象是null时,都返回false
System.out.println(null instanceof Object); //false
System.out.println(Object.class.isInstance(null)); //false
常用关键字
- abstract:只能修饰类和方法,只能在抽象类或接口中使用,接口方法默认是抽象的。
- 抽象类不能直接实例化。可以
new A() {}
实例化,实际上是创建了一个匿名内部类 - 抽象方法不能有具体实现,需要由子类实现方法。
- 不能和private、final、static、synchronized、native共存。修饰静态内部类的时候可以和static共存。(native是要求原生实现,abstract是要求子类实现)
- 抽象类不能直接实例化。可以
- final表示不可修改。
- 修饰变量:表示不可修改,即常量。对于基本数据类型表示值不可改变,对于引用数据类型,表示引用地址不可改变
- 修饰类:表示不可被继承
- 修饰方法:表示不可被重写。
- static表示静态:修饰成员变量、方法、内部类、代码块
- 和类绑定,不和对象实例绑定。
- 只在类首次加载的时候初始化,在内存中只有一个副本,存储在方法区中。
- 静态方法或静态代码块中,不能使用非静态变量。(非静态变量需要实例化对象,而静态方法不需要实例化对象就可以使用)
- 需要使用
类名.
进行引用,直接使用对象引用会有警告。反编译之后发现
//定义一个A类和一个静态方法
//源代码如下
new A().funStatic();
//反编译结果如下
new A();
A.funStatic();
数组定义和初始化
- 动态初始化:先声明数组大小,之后赋值,由系统分配默认值。基本数据类型使用各自的默认值,引用数据类型(包括String)默认值为null
- 静态初始化数组时,不必指明长度:
int[] a = {1, 2, 3}
- “[]” 是数组运算符的意思,在声明一个数组时,数组运算符可以放在数据类型与变量之间,也可以放在变量之后。
- 数组长度只能用short或int限定,否则会编译错误,如
char[] a = new char[1L]
- 定义多维数组时,其一维数组的长度必须首先指明,其他维数组长度可以稍后指定;
二维数组声明可以如下:
int a[][] = new int[10][10];
int []b[] = new int[10][10];
int [][]c = new int[10][10];
//多维数组可以先声明第一维长度,后面的长度可以稍后声明
int d[][] = new int[10][];
面向对象设计基本原则
SOLID原则+迪米特法则+合成复用原则,具体解释看设计模式-面向对象基本原则
- 单一职责(SRP,Single-Responsibility Principle):一个类只做一件核心的事,只有一个引起它变化的原因
- 开闭原则(OCP,Open-Closed Principle):对扩展开放,对修改关闭
- 里氏替换原则(LSP,Liskov-Substitution Principle):任何使用父类的地方,都能够被其子类替换,并且不影响程序运行结果。
- 接口隔离原则(ISP,Interface-Segregation Principle):将臃肿庞大的接口拆分成多个专门的小接口,让接口只包含客户感兴趣的方法。
- 依赖倒置原则(DIP,Dependency-Inversion Principle):高层模块不依赖于底层模块,二者都依赖于抽象。抽象不应该依赖具体,具体依赖于抽象
- 迪米特法则(LoD,Law of Demeter):也叫做最少知识原则。一个对象对其他对象应当尽可能少的了解。
- 合成复用原则(CRP,Composite Reuse Principle):也叫组合/聚合复用原则。简单来说就是多组合,少继承。
结语
参考文章: