目录
1 继承
1.1 继承概述
多个类中存在相同属性和行为时,将这些内容抽取到单独一个类中,那么多个类无需再定义这些属性和行为,只要继承那一个类即可。其中:
- 多个类可以称为子类,也叫派生类,
- 单独那一个类称为父类、超类或者基类。
继承:就是子类继承父类的属性和行为,使得子类对象具有与父类相同的属性、相同的行为。子类可以直接访问父类中的
非私有
的属性和行为。
❗️注意:
- 子类其实是可以继承父类的私有成员的,只是不可以直接访问
- 对于父类的静态成员,子类可以直接访问,但是这并不是继承,而是本来就是共享的
通过
extends
关键字,可以声明一个子类继承另外一个父类,继承的格式如下:
class 父类名 {
...
}
class 子类名 extends 父类名 {
...
}
继承的作用:
-
❤️ 优点:
- 大大地提高代码的复用性、维护性
- 类与类之间产生了关系,是多态的前提
- 子类可以拥有父类的内容,还可以拥有自己专有的内容
-
💔 弊端:
- 继承是侵入性的,降低了代码的灵活性(子类必须拥有父类的非私有属性和方法,让子类自由的世界多了些约束)
- 增强了代码的耦合性(代码和代码之间存在关联都可以将其称为耦合)
💨 继承的应用场景:
-
使用继承,需要考虑类与类之间是否存在
is..a
的关系,不能盲目使用继承-
is..a
的关系:谁是谁的一种,例如:老师和学生是人的一种,那人就是父类,学生和老师就是子类
-
继承的设计规范:
- 子类们相同的特征(共性属性、方法)放在父类中定义,子类独有的属性和行为应该定义在子类自己里面
1.2 继承的特点
1️⃣ 子类可以继承父类的属性和方法,但是子类不能继承父类的构造器(子类有自己的构造器,父类构造器用于初始化父类对象)
2️⃣ Java 中所有的类都是 Object 类的子类
3️⃣ Java 只支持单继承,
不支持多继承
,但
支持多层继承
- 单继承:子类只能继承一个直接父类
- 多继承:子类同时继承多个直接父类(Python语言是支持多继承的)
- 多层继承:子类A可以继承父类B,父类B可以继承父类C
4️⃣ 继承中成员变量的访问特点
在子类方法中访问一个变量(满足就近原则):
- 首先,在子类局部范围查找
- 其次,在子类成员范围查找
- 最后,在父类成员范围查找
🙋举个栗子:
父类
public class Fu {
int num = 10;
}
子类
public class Zi extends Fu {
int num = 20;
public void method() {
int num = 30;
System.out.println(num); // 30,局部变量
System.out.println(this.num); // 20,本类的成员变量
System.out.println(super.num); // 10,父类的成员变量
}
}
测试类
public class Test {
public static void main(String[] args) {
Zi zi = new Zi();
zi.method();
}
}
❗️ 注意:
- 如果子父类当中,出现了重名的成员变量,通过就近原则,会优先使用子类的,如果一定要使用父类的,可以通过
super
关键字进行区分
- 局部变量: 直接写成员变量名
- 本类的成员变量:
this.成员变量名
- 父类的成员变量:
super.成员变量名
5️⃣ 继承中成员方法的访问特点
通过子类对象访问一个方法(满足就近原则)
- 首先,在子类成员范围查找
- 最后,在父类成员范围查找
🙋举个栗子:
父类
public class Fu {
public void show(){
System.out.println("父类的show方法");
}
}
子类
public class Zi extends Fu {
public void show(){
System.out.println("子类的show方法");
}
public void method(){
this.show();
super.show();
}
}
测试类
public class Test {
public static void main(String[] args) {
Zi zi = new Zi();
zi.show();
// 子类的show方法
zi.method();
// 子类的show方法
// 父类的show方法
}
}
6️⃣ 继承中构造方法的访问特点
子类中所有的构造方法都会默认访问父类中的无参构造方法,再执行自己
- 原因:子在初始化的时候,有可能会使用到父类中的数据,如果父类没有初始化完,子类将无法使用父类的数据(子类初始化之前,一定要先完成父类的初始化)
-
如何初始化:构造方法的第一条语句默认都是:
super();
,这句代码被系统隐藏了,通过这句代码访问父类的无参构造方法
如果我们编写的类,没有手动指定父类,系统也会自动继承 Object类(Java体系中最顶层父类)
🙋举个栗子:
父类 Person
public class Person extends Object {
private String name;
private int age;
public Person(){
// super();
System.out.println("我是父类的空参数构造方法");
}
public Person(String name, int age){
// super();
this.name = name;
this.age = age;
System.out.println("我是父类的带参数构造方法");
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
子类 Student
public class Student extends Person {
// 子类自己特有的属性.
private int score;
public Student(){
// super();
System.out.println("我是子类的空参数构造方法..........");
}
public Student(int score){
// super();
this.score = score;
System.out.println("我是子类的带参数构造方法!!!");
}
/*public Student(String name,int age,int score){
super(name,age); // 初始化父类的变量
this.score = score; // 初始化自己本类的变量
System.out.println("我是子类的带参数构造方法!!!");
// super调用父类有参构造器的作用是什么?初始化继承自父类的数据
}*/
public int getScore() {
return score;
}
public void setScore(int score) {
this.score = score;
}
}
测试类
public class Test {
public static void main(String[] args) {
Student stu1 = new Student();
Student stu2 = new Student(100);
// Student stu3 = new Student("微微",21,100);
// System.out.println(stu3.getName()+"\t"+stu3.getAge()+"\t"+stu3.getScore());
}
}
结果输出:
我是父类的空参数构造方法
我是子类的空参数构造方法..........
我是父类的空参数构造方法
我是子类的带参数构造方法!!!
内存图解
父类中成员变量
name
,
age
都是私有的,为什么能够通过
super(name,age);
继承到呢?
- 父类中私有内容子类的确不可以直接访问,但是并不意味着子类继承不到。
❓问:如果父类中没有空参构造方法,只有带参数的构造方法,会报错(因为子类默认是调用父类的无参构造器的),该如何解决?
-
子类可以手动地通过
super(...)
访问父类的带参构造方法
public class Test2 {
public static void main(String[] args) {
Zi z = new Zi();
}
}
class Fu {
int age;
// 空参数构造方法
/*public Fu(){
System.out.println("父类空参数构造方法");
}*/
// 带参数构造方法
public Fu(int age){
this.age = age;
}
}
class Zi extends Fu {
/* 父类中没有空参构造方法,而子类构造方法中默认会通过super();访问父类的无参构造方法,因此会报错
public Zi(){
}*/
public Zi(){
// 由于super和this不能共存,所以此时子类的空参构造中不存在super();语句
this(10); // 访问本类的带参构造方法
}
public Zi(int age){
super(age); // 该构造方法又通过该语句手动调用父类的带参构造
}
}
❗️ 注意:
- 一个Java文件中允许存在多个class,但是这多个class中只能有一个被public修饰且该class类名必须和文件名保持一致
this(…)
和
super(…)
都必须放在构造方法的第一行有效语句,并且二者不能共存
1.3 方法的重写
在继承体系中,子类出现了和父类中一模一样(方法的名称一样,参数列表也一样)的方法声明,我们就称子类这个方法就是重写的方法
- 区别于重载(Overload):方法的名称一样,参数列表【不一样】
方法重写的应用场景:
- 当子类需要父类的功能,而功能主体子类有自己特有内容时,可以重写父类中的方法,这样,即沿袭了父类的功能,又定义了子类特有的内容
🙋举个栗子:
父类
public class Fu {
public void method(){
System.out.println("说中文");
}
}
子类
public class Zi extends Fu{
/*
在idea中输入方法名再回车即可自动生成重写方法,
@Override用于检查当前的方法是否是一个正确的重写方法,
这个注解就算不写,只要满足要求,也是正确的方法覆盖重写。
*/
@Override
public void method(){
super.method();
System.out.println("说外文");
}
}
测试类
public class Test {
public static void main(String[] args) {
Zi zi = new Zi();
zi.method();
// 说中文
// 说外文
}
}
❗️注意:
- 重写方法的名称、参数列表必须和被重写方法的名称、参数列表一致
- 父类中私有方法不能被重写(父类私有成员子类是不能直接访问的)
- 子类不可以重写父类的静态方法(如果重写,添加
@Override
验证会报错)- 子类重写父类的方法时,其
访问权限
必须
大于等于
父类
1.4 包与权限修饰符
1.4.1 包
包是用来分门别类的管理各种不同类的,类似于文件夹、建包有利于程序的管理和维护
建包的语法格式:
package 公司域名倒写.技术名称 // 包名建议全部英文小写,且具备意义
例如:
package com.baidu.javabean
关于导包:
- 相同包下的类可以直接访问
-
不同包下的类必须导包才可以使用,导包格式:
import 包名.类名;
- 如果这个类要使用不同包下相同的类名,此时默认只能导入一个类的包,另一个类要使用全名(包名+类名)访问
🙋举个栗子:
❗️注意:
- 建包语句必须在第一行,一般IDEA会自动帮忙创建
1.4.2 权限修饰符
Java中有四种权限修饰符:
public | protected | (default) | private | |
---|---|---|---|---|
同一个类 | YES | YES | YES | YES |
同一个包 | YES | YES | YES | NO |
不同包子类 | YES | YES | NO | NO |
不同包非子类 | YES | NO | NO | NO |
❗️注意:
- (default)并不是关键字“default”,而是根本不写。
- 权限大小:
public
>
protected
>(default)>
private
- 编写代码时,如果没有特殊的考虑,建议这样使用权限:
- 成员变量使用
private
,隐藏细节- 构造方法使用
public
,方便创建对象- 成员方法使用
public
,方便调用方法
1.5 final 关键字
1.5.1 final 的用法
final
关键字是最终的意思,可以修饰(方法、变量、类)
- 修饰方法:表明该方法是最终的方法,不能被重写
-
修饰变量:表明该变量是常量,不能被再次赋值
- 基本数据类型变量:数据值不能被更改
- 引用数据类型变量:地址值不能被更改,但是地址值里面的内容是可以发生改变的
-
final 修饰成员变量的初始化时机:
- 在创建的时候,直接给值;
- 在构造方法结束之前,完成赋值
- 修饰类:表明该类是最终类,不能被继承
🙋举个栗子:
public static void main(String[] args) {
final int A = 10;
final int MAX = 10;
final int MAX_VALUE = 20;
final Student stu = new Student();
stu.setName("张三");
stu.setName("李四");
// stu = new Student(); stu 是引用数据类型的变量,让stu重新记录一份内存地址
class Student {
// 初始化时机一
final int a = 10;
private String name;
// 初始化时机二
/*final int a;
public Student(){
a =10;
}*/
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}
1.5.2 常量
常量是使用了
public static final
修饰的成员变量,
必须有初始化值
,而且执行的过程中其值不能改变
常量的作用和好处:
- 可以用于做系统的配置信息,方便程序的维护,同时提高可读性
常量的执行原理:
-
在编译阶段会进行“宏替换”,把使用常量的地方全部替换成真实的字面量
- 这样做的好处是让使用常量的程序的执行性能与直接使用字面量是一样的(既有优点又保持原有的性能)
❗️ 注意:
- 常量的命名规范:如果是一个单词,所有字母大写,如果是多个单词,所有字母大写,但是中间需要使用
_
分隔
1.6 枚举
枚举是Java中的一种特殊类型,其作用是为了做信息的标志与分类
定义枚举类的格式:
修饰符 enum 枚举名称{
第一行都是罗列枚举实例的名称
}
例如:
public enum Season{
SPRING,SUMMER,AUTUMN,WINTER;
}
通过
javac Season.java
进行编译得到
Season.class
,再对
Season.class
进行反编译
javap Season.class
❤️ 反编译后观察枚举的特征:
-
枚举类都是继承了枚举类型:
java.lang.Enum
- 枚举都是最终类,不可以被继承
- 枚举的构造器是私有的,枚举对外不能创建对象
- 枚举只能有内部的几个对象,相当于多例模式
😦 选择常量、枚举做信息标志和分类的区别
- 常量:虽然可以实现可读性,但是入参值不受约束,代码相对不够严谨
- 枚举:代码可读性好,入参约束严谨,代码优雅,是最好的信息分类技术,建议使用
2 抽象类
2.1 抽象类概述
在Java中
abstract
是抽象的意思,可以修饰类、成员方法。
将共性的行为(方法)抽取到父类之后,发现该方法的实现逻辑无法在父类中给出具体明确,该方法就可以定义为抽象方法。
abstract
修饰方法,这个类就是抽象方法,其格式为:
修饰符 abstract 返回值类型 方法名 (参数列表);
abstract
修饰类,这个类就是抽象类,其格式为:
abstract class 类名称 { }
❗️ 注意:
- 抽象方法只有方法声明,没有方法体
- 如果一个类中存在抽象方法,该类就必须声明为抽象类。
2.2 抽象类的使用
案例需求
- 定义猫类(Cat)和狗类(Dog)
- 猫类成员方法:eat(猫吃鱼)drink(喝水…)
- 狗类成员方法:eat(狗吃肉)drink(喝水…)
实现步骤
- 猫类和狗类中存在共性内容,应向上抽取出一个动物类(Animal)
- 父类Animal中,无法将 eat 方法具体实现描述清楚,所以定义为抽象方法
- 抽象方法需要存活在抽象类中,将Animal定义为抽象类
- 让 Cat 和 Dog 分别继承 Animal,重写eat方法
- 测试类中创建 Cat 和 Dog 对象,调用方法测试
Animal 类
public abstract class Animal { // 该类中有抽象方法故必须定义为抽象类
public void drink(){
System.out.println("喝水");
}
public abstract void eat(); // 定义的抽象方法
}
Dog 类
public class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗吃肉");
}
}
Cat 类
public class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫吃鱼");
}
}
Test 类
public static void main(String[] args) {
Dog d = new Dog();
d.eat(); // 狗吃肉
d.drink(); // 喝水
Cat c = new Cat();
c.drink(); // 喝水
c.eat(); // 猫吃鱼
//Animal a = new Animal(); // 抽象类不能创建对象
//a.eat();
}
❗️注意:
- 类有的成员(成员变量、方法、构造器)抽象类都具备
抽象类不能实例化(创建对象)
- 抽象类的子类, 必须重写父类中
所有
的抽象方法,否则可以将自己也变成一个抽象类- 抽象类中,可以是抽象方法也可以是非抽象方法,甚至可以一个抽象方法都不写;但是有抽象方法的类一定是抽象类
- 不能用
abstract
修饰变量、构造器、代码块
✨ 拓展:
final
和
abstract
是什么关系?
- 互斥关系
- 修饰类:
abstract
定义的抽象类作为模版让子类继承,而
final
定义的类是不能被继承的- 修饰方法:抽象方法定义通用功能让子类重写,而
final
定义的方法子类不能重写
2.3 模板设计模式
-
设计模式
设计模式(Design pattern)是一套被反复使用、多数人知晓的、经过分类编目的、代码设计经验的总结。使用设计模式是为了可重用代码、让代码更容易被他人理解、保证代码可靠性、程序的重用性。
-
模板设计模式
把抽象类整体就可以看做成一个模板,模板中不能决定的东西定义成抽象方法
让使用模板的类(继承抽象类的类)去重写抽象方法实现需求 -
模板设计模式的优势
模板已经定义了通用结构,模板方法不能确定的部分定义成抽象方法,交给子类实现,使用者只需要关心自己需要实现的功能即可,同时提高了代码的复用性
-
模板设计模式的使用场景
当系统中出现同一个功能多处在开发,而该功能大部分代码是一样的,只有其中部分可能不同的时候
🙋举个栗子:
作文模板类
public abstract class CompositionTemplate {
// 模版方法
public final void write(){
System.out.println("<<我的爸爸>>");
// 抽象方法
body();
System.out.println("啊~ 这就是我的爸爸");
}
public abstract void body();
}
模版方法使用
final
修饰会更专业、更安全,防止子类重写
实现类
public class Tom extends CompositionTemplate {
@Override
public void body() {
System.out.println("那是一个秋天, 风儿那么缠绵,记忆中, " +
"那天爸爸骑车接我放学回家,我的脚卡在了自行车链当中, 爸爸蹬不动,他就站起来蹬...");
}
}
测试类
public class Test {
public static void main(String[] args) {
Tom t = new Tom();
t.write();
}
}
3 接口
3.1 接口的概述
当一个类中的
所有方法都是抽象方法
的时候,我们就可以将其定义为
接口
,接口也是一种
引用数据类型
(数组、类、接口),它比抽象类还要抽象。
接口,是方法的集合,如果说类的内部封装了成员变量、成员方法和构造方法,那么 接口的内部主要就是封装了方法,包含常量、抽象方法(JDK 7及以前),默认方法和静态方法(JDK 8),私有方法 (JDK 9)。
接口的存在意义:
- 规则的定义
- 程序的扩展性
接口的定义与定义类方式相似,但是使用
interface
关键字,其格式如下:
public interface 接口名{}
它不能创建对象(实例化),但是可以被
实现
,使用
implements
关键字 ,类似于被继承,格式如下:
public class 类名 implements 接口名{}
一个实现接口的类(可以看做是接口的子类)
- 要么需要重写接口中所有的抽象方法
- 要么是一个抽象类
🙋举个栗子:
接口类
public interface Inter {
public abstract void study();
// 由于接口体现规范思想,规范默认都是公开的,所以代码层面public abstract可以省略不写
// void study();
}
实现类
public class InterImpl implements Inter {
@Override
public void study() {
System.out.println("我是实现类中的study方法");
}
测试类
public class TestInterface {
public static void main(String[] args) {
InterImpl ii = new InterImpl();
ii.study(); // 我是实现类中的study方法
}
}
❗️注意:
- 接口,也是一种规范,规范一定是公开的。
- Java中不支持多继承,但是支持多实现
public class 类名 implements 接口1,接口2{}
- 定义接口时换成了关键字
interface
之后,编译生成的字节码文件仍然是:
.java
—>
.class
3.2 接口中成员的特点
1️⃣ 成员变量(常量)
- 接口的实现类可以直接使用接口当中定义的成员变量
-
只能是常量,系统会默认给接口的成员变量加入三个关键字
public
、
static
(可以以通过
接口名.
调用成员变量)、
final
,这几个关键字可以省略不写
2️⃣ 成员方法(抽象方法)
只能是抽象方法,系统会默认加入两个关键字
public
、
abstract
,这几个关键字可以省略不写
❗️注意:
- 对于“接口中的成员方法只能是抽象方法”只在JDK7以及之前的版本是正确的
3.3 类与接口的关系
1️⃣ 类与类
- 继承关系,只能单继承,不能多继承,可以多层继承
2️⃣ 类与接口
-
实现关系,可以单实现,也可以多实现,还可以在继承一个类的同时实现多个接口
- 如果直接父类和接口中出现了相同的方法声明,但是代码逻辑不一样,优先使用直接父类的代码逻辑
- 如果一个类实现了多个接口,多个接口存在同名的默认方法,该类就必须对该方法进行重写
- 如果一个类实现了多个接口,多个接口中有同名的静态方法并不冲突
🙋举个栗子:
父类
public class Fu {
public void show(){
System.out.println("Fu...show");
}
}
接口类
public interface Inter {
public default void show(){
System.out.println("Inter....show");
}
}
实现类与测试类
public class TestInterface {
public static void main(String[] args) {
InterImpl ii = new InterImpl();
ii.show(); // Fu...show
}
}
class InterImpl extends Fu implements Inter {
}
3️⃣ 接口与接口
- 继承关系,可以单继承,也可以多继承
作用:规范合并,合并多个接口中的功能到同一个接口中,便于子类实现(只需要实现一个接口就可以了)
🙋举个栗子:
接口A类
public interface InterA {
public abstract void showA();
public default void method(){
System.out.println("InterA...method方法");
}
}
接口B类
public interface InterB {
public abstract void showB();
public default void method(){
System.out.println("InterB...method方法");
}
}
接口C类
// InterA,InterB出现了相同的方法声明method(),但是代码逻辑不一样,子类不知道该继承谁,自己重写该方法
public interface InterC extends InterA , InterB {
@Override
public default void method() {
System.out.println("InterC接口,解决代码逻辑冲突问题, 重写method方法");
}
}
实现类
// 实现类需要重写接口类中所有的抽象方法
public class InterImpl implements InterC {
@Override
public void showA() {
}
@Override
public void showB() {
}
}
测试类
public class TestInterface {
public static void main(String[] args) {
InterImpl ii = new InterImpl();
ii.method(); // InterC接口,解决代码逻辑冲突问题, 重写method方法
}
}
3.4 JDK8开始接口新增方法
3.4.1 JDK8
❓ 问题:当对接口丰富,加入多个新的抽象方法,那么接口的实现类也需要对新加入的方法进行重写,如何能在丰富接口功能的同时又不对实现类代码进行更改?
❤️ 解答:
允许接口中定义带有方法体的方法
在JDK8版本之后,Java只对接口的成员方法进行了改进
-
允许在接口中定义非抽象的方法,但是需要使用关键字
default
修饰,这些方法就是默认方法(解决接口升级问题) -
接口中允许定义
static
,即静态方法
1️⃣ 默认方法
类似于之前写的普通实例方法,只是在这里必须使用
default
修饰,
需要用接口的实现类的对象来调用
。
接口中默认方法的定义格式:
public default 返回值类型 方法名(参数列表){}
❗️注意:
- 接口中的默认方法的权限修饰符都是
public
,就算不写,系统也会默认加上,但是
default
关键字不能省略- 默认方法不是抽象方法,所以不强制被重写,但是可以被重写,
实现类重写接口中的默认方法时,要去掉
default
关键字
一个类如果实现了多个接口
,
多个接口存在同名的默认方法
,不冲突,
该类必须对该方法进行重写即可
2️⃣ 静态方法
接口中静态方法的定义格式:
public static 返回值类型 方法名(参数列表){}
❗️注意:
- 接口的静态方法只能通过本身的接口名调用,不能通过实现类名或者实现类对象名调用
- 所以如果一个类实现多个接口,多个接口中有同名的静态方法并不冲突
public
同样可以省略,但是
static
不能省略
3.4.2 JDK9
当两个默认方法或者静态方法中包含一段相同的代码实现时,程序必然考虑将这段实现代码抽取成一个共性方法,而这个共性方法是不需要让别人使用的,因此用私有给隐藏起来,这就是Java 9 增加私有方法的必然性。
只能在本类(本接口)中使用。
接口中私有方法的定义格式:
// 格式1
private 返回值类型 方法名(参数列表){}
// 格式2
private static 返回值类型 方法名(参数列表){}
🙋举个栗子:
接口类
public interface Inter {
public default void start() {
System.out.println("start方法执行了...");
log();
}
public default void end() {
System.out.println("end方法执行了...");
log();
}
private void log(){
System.out.println("日志记录 ( 模拟 )");
}
private static void check(){
System.out.println("权限校验 ( 模拟 )");
}
public static void open() {
check();// 静态方法只能访问静态成员
System.out.println("open方法执行了");
}
public static void close(){
check();
System.out.println("close方法执行了");
}
}
实现类和测试类
public class TestInterface {
public static void main(String[] args) {
InterImpl ii = new InterImpl();
ii.start();
ii.end();
Inter.open();
Inter.close();
}
}
class InterImpl implements Inter {
}