目录
一、final关键字
final:最终的意思,可以修饰:类、方法、变量
修饰类:被当做最终类,无法继承,适用场景:工具类,直接输出功能的类。
修饰方法:被当做最终方法,子类继承后无法重写方法。
修饰变量:一般用于修饰静态成员变量,称为常量,有且仅能被赋值一次,适用于定义系统名称、配置信息等不常变更变量值的场景。修饰实例变量一般没有意义,实例变量(对象变量)就是用于不同对象赋值使用。
注意事项:
①常量命名规范:使用大写英文单词,多个词使用下划线连接
②final修饰基本类型的变量,变量存储的数据不能被改变
③但final修饰引用类型的变量,变量存储的地址不能被改变,但地址所指向的内容是可以被改变的,示例:
public class FinalDemo {
public static void main(String[] args) {
final int[] arr = { 1, 2, 3, 4, 5};
// arr = new int[]{5,6,7,8,9}; 报错,重新赋值等于改变引用类型变量的存储地址
// 但引用类型变量的值是可以修改的
arr[0] = 5;
System.out.println(arr[0]); // 打印结果:5
}
}
⑤常量的优势:代码可读性好,可维护性也更好
⑥使用常量不会影响性能,程序编译后,常量会被“宏替换”,在out目录中查看编译后的.class查看会发现使用常量的地方都被替换为了字面量(常量的值),确保使用常量和直接使用字面量的性能一致
二、单例类
1.什么是设计模式?
设计模式:用于解决问题的最优方案,理解设计模式主要搞懂该模式解决什么问题、如何写。单例类就是一种设计模式,单例涉及模式解决的问题是确保某个类只能创建一次对象。
2.单例模式写法(饿汉式单例)
//设计单例类
public class A {
//1.私有化构造方法
private A() {
}
//2.创建静态成员变量并且创建好对象
private static A a = new A();
//3.创建静态方法,返回静态成员变量
public static A getInstance() {
return a;
}
}
注意:若第二步静态成员变量使用public和final修饰,则可不需要第三步,可直接调用。不加final的情况下避免使用public,否则调用变量时可将变量值对应的对象重新赋值为空:A.a = null; 导致异常。
使用验证:
public class Test {
public static void main(String[] args) {
A a = A.getInstance();
A b = A.getInstance();
//两个对象是同一个对象,输出的内存地址是相同的
System.out.println(a); // oop.singleinstance.A@1b6d3586
System.out.println(b); // oop.singleinstance.A@1b6d3586
//打印结果: true
System.out.println(a == b);
}
}
总结:
①单例的写法:私有构造器,定义静态变量存储对象,并提供静态方法返回对象
②饿汉式单例的特点:在获取类的对象时,对象已经创建好了(第二步)
③应用场景:功能只需要运行一个,例如:JVM虚拟机、任务管理器,避免运行多个造成资源浪费
3.懒汉式单例写法
//认识懒汉单例模式
public class B {
//1.私有化构造方法
private B(){}
//2.创建一个静态变量,用于保存对象,但初始值是null
private static B b ;
//3.创建一个静态方法,返回对象,如果对象为null,则创建对象,并返回
public static B getInstance(){
if(b == null){
b = new B();
}
return b;
}
}
总结:
①与饿汉式的区别在第二、三步,定义存储对象的变量时,初始值为空(饿汉式初始化时已赋值对象)
②懒汉式单例特点:使用时才创建对接(延迟加载对象)
③使用过一次后,例如创建对象B b = B.getInstance();后,多次使用均返回的是同一对象
三、枚举类
1.枚举类的写法
使用enum修饰类名即为枚举类
//定义枚举类,类名使用enum关键字修饰
public enum A {
//枚举类的第一行只能是枚举对象,也是常量
x, y, z;
}
反编译class文件结果:
使用枚举类:
public class Tets {
public static void main(String[] args) {
//打印结果不为内存地址,枚举类已自动包含父类的toString()方法
System.out.println(A.x); //打印结果:x
System.out.println(A.y); //打印结果:y
System.out.println(A.z); //打印结果:z
//使用父类的方法打印枚举对象在数组中的索引(反编译图中显示枚举类已自动创建类的数组存枚举对象)
System.out.println(A.x.ordinal()); //打印x在数组A中的索引,打印结果:0
}
}
总结:
①枚举类使用enum修饰类名,枚举类是最终类,无法被继承(反编译内容中类名使用了final关键字),且枚举类都继承自java.lang.Enum类
②枚举类第一行只能是枚举对象,也是常量(反编译内容中使用了final修饰),并且每个常量会初始化枚举的对象
③枚举类的构造器默认且只能是私有,因此无法创建对象
④除了继承自父类java.lang.Enum的方法,编译器会自动新增几个方法(见反编译内容图)
2.枚举类的使用场景
适合信息分类或标志,例如一个游戏可以上下左右移动元素,需要获取用户操作并传给实现移动元素的功能,那么移动的方向即可作为枚举类的枚举值。先创建枚举类:
//创建枚举类
public enum B {
//定义四种可能的方向
UP, DOWN, LEFT, RIGHT;
}
调用移动功能传入枚举值:
public class Test2 {
public static void main(String[] args) {
//调用方法,传入实际操作的枚举值
move(B.LEFT); //打印结果:向左移动
}
//定义实现移动功能的方法
public static void move(B b) {
switch (b) {
case UP:
System.out.println("向上移动");
break;
case DOWN:
System.out.println("向下移动");
break;
case LEFT:
System.out.println("向左移动");
break;
case RIGHT:
System.out.println("向右移动");
break;
}
}
}
总结:该案列通过将用户的操作进行信息的分类并标志将用户的操作结果以参数的形式传给实现移动的方法,实现功能。也可以通过将用户可能的操作定义为常量实现,但缺点是传给移动方法的参数无法控制,可能会传递不存在的常量参数影响功能执行。而当移动方法定义参数类型为枚举类时,可对传递参数进行限制,只能传递枚举类中定义的枚举值。
四、抽象类
1.抽象类写法
//创建抽象类
public abstract class A {
//创建抽象方法
public abstract void show();
}
注意事项:
①抽象类或抽象方法使用abstract修饰,抽象方法不能有方法体,只能有方法声明
②抽象类中可以不写抽象方法,但是存在抽象方法的类必须是抽象类
③类有点成员(成员变量、构造器、方法)抽象类都具备
④抽象类不能创建对象,只能被继承,且继承的类必须重新父类中的抽象方法,否则也要继续定义为抽象类
2.抽象类应用场景一
需求:实现可以输出不同动物叫声的功能
思路:先定义一个抽象父类,并创建抽象方法用于输出动物叫声(每个动物叫声不同,在每个动物子类继承父类时都需要重写叫声方法,所以使用抽象方法声明即可,无需再写方法体)
//创建抽象类
public abstract class Animal {
//定义抽象方法,实现动物叫声功能
public abstract void show();
}
创建子类猫和子类狗重写叫声方法
//创建子类猫继承抽象类Animal
public class Cat extends Animal{
//重写抽象方法
@Override
public void show() {
System.out.println("猫的叫声:喵喵喵");
}
}
////创建子类狗继承抽象类Animal
public class Dog extends Animal{
//重写抽象方法
@Override
public void show() {
System.out.println("狗的叫声:旺旺旺");
}
}
输出对应动物的叫声
public class Test {
public static void main(String[] args) {
Animal a = new Dog();
a.show(); //打印结果:狗的叫声:旺旺旺
}
}
总结:父类知道每个子类要做某个行为,但每个子类的做法不同时,父类即可定义为抽象想法,交给子类重写实现不同做法,更好的支持多态
3.抽象类应用场景二——模板方法设计模式
目的:提高代码复用性、简化子类设计。比如2个类的第一个功能步骤代码一致,但第二个步骤存在差异,即可将一致的内容作为父类,子类继承后实现差异的内容(通用步骤给父类,子类完成独有步骤)
需求:张三和李四需要做自我介绍,介绍内容共三段,其中第一段和第三段内容一致,只有第二段不同
思路:将第一段和第三段定义在抽象父类中,且将第二段定义为抽象方法,通过子类重写实现差异化
创建父类:
public abstract class Student {
//定义模板方法,使用final修饰避免被子类重写
public final void print() {
//第一段固定内容
System.out.println("我的学校是ABC小学");
//通过子类重写抽象方法实现第二段的内容差异
name();
//第二段重写内容
System.out.println("我来自二年级一班");
}
//定义抽象方法
public abstract void name();
}
创建张三子类并重写抽象方法:
public class Zhangsan extends Student{
@Override
public void name() {
System.out.println("我是张三,今年7岁,性别男");
}
}
创建李四子类并重写抽象方法:
public class Lisi extends Student{
@Override
public void name() {
System.out.println("我是李四,今年8月,性别女");
}
}
应用结果:
public class Test {
public static void main(String[] args) {
Student s = new Zhangsan();
s.print();
//打印结果:
//我的学校是ABC小学
//我是张三,今年7岁,性别男
//我来自二年级一班
}
}
模板方法设计模式总结:
①定义抽象类
②定义一个模板方法,将共同的内容放入方法体
③定义一个抽象方法,将存在差异的步骤交给子类继承时重写实现
④模板方法建议使用final修饰,避免被子类重写后失效
五、接口
1.理解接口
1)类名使用interface定义,在JDK8之前,接口中只能定义常量和抽象方法
创建接口A:
public interface A {
//接口中定义变量默认为常量,可省略public static final
String NAME = "张三";
//接口中定义方法默认是抽象方法,可省略public abstract
void run();
}
创建接口A2:
public interface A2 {
void go();
}
2)接口的使用:implements关键字。作用:被类实现,实现接口的类称为实现类,一个实现类可以实现多个接口
创建实现类Test:
public class Test implements A,A2{
@Override
public void run() {
}
@Override
public void go() {
}
}
总结:
①接口中定义的变量默认为常量,可省略public static final
②接口中定义方法默认是抽象方法,可省略public abstract
③实现类同时实现多个接口的作用等于子类同时继承了多个父类,必须重写全部接口的全部抽象方法,否则需将实现类定义为抽象类
2.接口的好处
弥补类单继承的不足,一个类可同时实现多个接口,使类的角色更多,功能更强大。让程序可以面向接口编程,可以灵活切换各种业务实现场景(更利于程序解耦合)
案例:存在父类人people,接口司机driver,接口主持人presenter,创建子类学生和老师时,同时实现司机和注册人接口,赋予老师和学生更多的角色。学生和老师既可以当司机,也能当注册人,在学生当司机效果不好时,直接切换老师当司机:Driver driver = new Teacher();
public class Test {
public void appTest() {
//面向接口编程,更利于解耦合
//让学生当司机
Driver driver = new Student();
//让老师当主持人
Presenter presenter = new Teacher();
}
//创建司机接口
interface Driver{}
//创建主持人接口
interface Presenter{}
//创建人属性的父类
class People{}
//创建学生子类并实现接口增加子类的属性
class Student extends People implements Driver,Presenter{}
//创建老师子类并实现接口增加子类的属性
class Teacher extends People implements Driver,Presenter{}
}
3.接口案例
需求:
①设计一个学生信息管理模块,包含数据:姓名、性别、成绩
②功能:要求打印出全班信息和平均分(方案一)
③功能:要求打印出全班信息和男女人数,且平均分需要排除最高分和最低分(方案二)
④打印时可以灵活切换方案一或者方案二(体现高度灵活和解耦合)
1)创建学生信息模板()
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
//创建学生类,属性为姓名,成绩,性别
public class Student {
private String name;
private Double score;
private char sex;
}
2)创建接口定义两个功能(打印学生信息和平均分)
//创建接口定义实现的两种功能:打印所有学生的信息,打印所有学生的平均分
public interface PrintDataInter {
void printAllStudentInfo();
void printAvgScore();
}
3)创建实现类完成方案一:打印出全班信息和平均分
//创建实现第一种方案的实现类
public class PrintDataInterImp1 implements PrintDataInter{
//创建用于存储学生信息的数组
private Student[] students;
//创建有参构造方法接收数组
public PrintDataInterImp1(Student[] students) {
//赋值给成员变量
this.students = students;
}
//实现接口中的抽象方法:打印所有学生信息
@Override
public void printAllStudentInfo() {
System.out.println("所有学生信息如下:");
for (int i = 0; i < students.length; i++) {
Student s = students[i];
System.out.println(s.getName() + " " + s.getScore() + " " + s.getSex());
}
}
//实现接口中的抽象方法:打印平均分
@Override
public void printAvgScore(){
double sum = 0;
for (int i = 0; i < students.length; i++) {
Student s = students[i];
sum += s.getScore();
}
System.out.println("平均分是:" + sum / students.length);
}
}
4)创建实现类完成方案二:打印出全班信息和男女人数,且平均分需要排除最高分和最低分
public class PrintDataInterImp2 implements PrintDataInter{
//创建用于存储学生信息的数组
private Student[] students;
//创建有参构造方法接收数组
public PrintDataInterImp2(Student[] students){
//赋值给成员变量
this.students = students;
}
//实现接口中的抽象方法:打印所有学生信息并打印男女生人数
@Override
public void printAllStudentInfo() {
//初始化男生人数变量
int manNum = 0;
System.out.println("所有学生信息如下:");
for (int i = 0; i < students.length; i++) {
Student s = students[i];
//打印学生信息
System.out.println(s.getName() + " " + s.getScore() + " " + s.getSex());
//判断性别
if (s.getSex() == '男') {
//统计男生人数
manNum++;
}
}
System.out.println("男生人数为:" + manNum);
System.out.println("女生人数为:" + (students.length - manNum));
}
//实现接口中的抽象方法:打印除开最高分和最低分的平均成绩
@Override
public void printAvgScore() {
//初始化存储最高分和最低分的变量
double max = students[0].getScore();
double min = students[0].getScore();
//初始化存储总成绩的变量
double sum = students[0].getScore();
for (int i = 1; i < students.length; i++) {
Student s = students[i];
sum += s.getScore();
if (s.getScore() > max) {
max = s.getScore();
}
if (s.getScore() < min) {
min = s.getScore();
}
}
System.out.println("最高分是:" + max);
System.out.println("最低分是:" + min);
System.out.println("除最高分和最低分之外的平均成绩为:" + (sum - max - min) / (students.length - 2));
}
}
5)创建接口的实现类对象,通过调整实现类名称切换方案一和方案二即可打印不同信息
public class Test {
public static void main(String[] args) {
//定义数组创建6个学生数据
Student[] allStudents = new Student[6];
allStudents[0] = new Student("张三", 92.5, '男');
allStudents[1] = new Student("李四", 85.5, '女');
allStudents[2] = new Student("王五", 95.5, '男');
allStudents[3] = new Student("赵六", 90.5, '女');
allStudents[4] = new Student("孙七", 93.5, '男');
allStudents[5] = new Student("周八", 91.5, '女');
//创建接口的实现类对象,需要切换方案时仅需修改实现类名的1和2,不需要修改后续的代码,体现解耦合
PrintDataInter printDataInter = new PrintDataInterImp2(allStudents);
//调用实现类的方法打印所有学生的信息
printDataInter.printAllStudentInfo();
//调用实现类方法打印平均分
printDataInter.printAvgScore();
}
}
4.JDK8开始新增的三种形式的方法
作用:增强接口的能力,便于项目的拓展和维护,比如已上线的项目,接口已被几十上百个实现类使用,若要新增功能(新增抽象方法)会导致接口的所有实现类需要重写抽象方法。但若可以新增非抽象方法来实现功能,那么只需要直接调用即可,无需所有实现类都重新新增方法。
1)新增的三种形式的方法:默认方法(普通实例方法)、私有方法、静态方法
public interface A {
//1、定义默认方法(普通实例方法)
//必须使用default修饰符修饰,才能定义默认方法
//接口不能创建对象,执行通过接口的实现类调用默认方法
//默认方法默认包含public,所以可以不写public
default void show(){
System.out.println("默认方法已执行");
//在本类方法中调用本类私有方法
show1();
}
//2、定义私有方法,JDK9开始支持在接口中添加私有方法
//接口的实现类无法调用私有方法,只能被当前类中的其他方法调用
private void show1(){
System.out.println("私有方法已执行");
}
//3、定义静态方法
//静态方法使用static修饰符修饰,且默认包含public,所以可以不写public
//静态方法不能被继承,只能通过接口名调用
static void show2(){
System.out.println("静态方法已执行");
}
}
2)使用接口中的三种方法
public class Test {
public static void main(String[] args) {
A a = new B();
//2、调用默认方法和私有方法
a.show(); //打印结果:默认方法已执行\n私有方法已执行
//3、调用静态方法,接口名.静态方法名
A.show2(); //打印结果:静态方法已执行
}
}
//1、创建B类实现接口A
class B implements A {}
5.接口的注意事项
1)接口可以多继承,对比:
类与类:单继承,一个类只能直接继承一个父类
类与接口:多实现,一个类可以实现多个接口
接口与接口:多继承,一个接口可以同时继承多个接口(当一个实现类要同时实现A/B/C接口时,可以通过C接口继承A/B接口,实现类只需实现C接口即可,写法:interface C extends A , B)
2)若多个接口中存在相同返回值且相同方法名的方法,被继承或被实现时会自动合并。但若方法名相同,返回值不同,被继承或被实现时则会报错。
3)子类继承父类的同时也实现了接口的情况下,若父类和接口中存在相同的方法,默认优先使用父类中的方法,示例:
public class Test {
public static void main(String[] args) {
//查看默认优先调用结果
C c = new C();
c.show(); //打印结果:执行了父类A的show方法
}
}
//定义父类A
class A {
public void show() {
System.out.println("执行了父类A的show方法");
}
}
//定义接口B
interface B {
default void show(){
System.out.println("执行了接口B的show方法");
};
}
//创建子类C继承父类A并实现接口B
class C extends A implements B {
}
若需强行调用接口的方法需在子类中创建方法进行中转:
public class Test {
public static void main(String[] args) {
//调用子类的go方法验证中转结果
C c = new C();
c.go(); //打印结果:执行了接口B的show方法
}
}
class A {
public void show() {
System.out.println("执行了父类A的show方法");
}
}
interface B {
default void show(){
System.out.println("执行了接口B的show方法");
};
}
class C extends A implements B {
//创建方法实现子类调用接口的方法
public void go() {
//格式:接口名.super.方法名
B.super.show();
}
}
4)一个类实现的多个接口中,存在相同的默认方法时,也会冲突,可通过在实现类重写解决。若非要调用一个或多个接口中的默认方法,可在实现类的重写方法中使用:接口名.super.默认方法名解决,示例:
public class Test {
public static void main(String[] args) {
C c = new C();
c.show(); //打印结果:执行了接口A的show方法\n执行了接口B的show方法
}
}
interface A {
default void show(){
System.out.println("执行了接口A的show方法");
};
}
interface B {
default void show(){
System.out.println("执行了接口B的show方法");
};
}
class C implements A,B {
@Override
public void show() {
A.super.show();
B.super.show();
}
}
6.抽象类和接口的对比
1)相同点:
①都是抽象形式,都可以有抽象方法,都不能创建对象
②都是派生子类形式,抽象类是被子类继承使用,接口是被实现类实现使用
③一个类继承抽象类,或者实现接口,需要重写抽象方法,否则自己要成为抽象类或报错
④都能支持多态,实现解耦合(抽象类可以面向多个子类切换,接口也可以面向多个实现类切换)
2)不同点:
①抽象类可以定义类的全部普通成员(成员变量、构造器、方法),接口只能定义常量、抽象方法以及JDK8之后新增的默认方法、静态方法、私有方法(JDK9)
②抽象类只能被类单继承,但接口可以被多实现(一个类继承了抽象类就不能再继承其他类,但一个类实现了接口还可以继承其他类或者实现其他接口)
③抽象类体现模板思想,更利于做父类,实现代码的复用性
④接口更适合做功能的解耦合:解耦合性更强更灵活(一个接口可以有N个实现类对象,可以随意切换实现类对象解耦合)
六、综合案例
需求:设计智能家居系统,可以让用户选择要控制的家电进行开关功能
思路:
设计接口,提供开关功能(抽象类);
设计父类,定义家电模板并实现开关接口;
针对各个家电创建子类继承家电父类;
设计智能家居类(单例类),实现显示所有家电和状态的方法以及操作开关的方法
1)设计接口提供开关抽象类
//定义开关接口
public interface Switch {
//定义实现家电开关的抽象方法
void press();
}
2)设计父类定义家电模板并实现开关接口
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
@NoArgsConstructor
@AllArgsConstructor
//创建家电父类并实现开关接口
public class JD implements Switch{
//定义家电名称
private String name;
//定义家电开关状态
private boolean status;
//重写开关接口方法
@Override
public void press() {
//状态取反,当前状态为true时,表示关机,按下开关后,状态变为false,即开机
status = !status;
}
}
3)创建电视、客厅灯、空调的子类
//创建子类电视继承家电父类
public class TV extends JD{
//定义有参构造方法将属性值传给父类进行初始化
public TV(String name, boolean status) {
super(name, status);
}
}
//创建子类电灯继承家电父类
public class Lamp extends JD{
//定义有参构造方法将参数传递给父类进行初始化
public Lamp(String name, boolean status) {
super(name, status);
}
}
//创建子类空调继承家电类父类
public class Air extends JD{
//定义有参数构造方法将属性值传给父类进行初始化
public Air(String name, boolean status) {
super(name, status);
}
}
4)设计家电控制的单例类,实现打印家电状态和开关操作
//创建家电控制的单例类
public class HomeControl {
//创建单例类步骤1:创建一个私有的构造方法,用于创建单例对象
private HomeControl(){}
//创建单例类步骤2:创建一个静态的成员变量,用于保存单例对象
private static final HomeControl homecontrol = new HomeControl();
//创建单例类步骤3:创建一个静态的成员方法,返回单例对象
public static HomeControl getHomecontrol(){
return homecontrol;
}
//定义打印所有家电名称和状态的方法
public void printAllJDInfo(JD[] jds){
System.out.println("所有家电状态如下:");
for (int i = 0; i < jds.length; i++) {
JD jd = jds[i];
System.out.println(i + 1 + "、" + jd.getName() + "的状态是:" + (jd.isStatus() ? "开" : "关"));
}
}
//定义开关家电的方法
public void switchJD(JD jd){
jd.press(); //调用JD类的press()方法实现开关家电
System.out.println("按下开关," + jd.getName() + "的状态变为:" + (jd.isStatus() ? "开" : "关"));
}
}
5)初始化家电数据并运行
import java.util.Scanner;
public class Test {
public static void main(String[] args) {
//创建家电数组初始化3个家电对象
JD[] jds = new JD[3];
jds[0] = new TV("小米电视", true);
jds[1] = new Lamp("客厅灯", true);
jds[2] = new Air("美的空调", false);
//创建家电控制单例类的对象
HomeControl hc = HomeControl.getHomecontrol();
//循环操作家电状态
while (true) {
//打印所有家电状态
hc.printAllJDInfo(jds);
System.out.print("请选择要操作的家电或输入exit退出:");
Scanner sc = new Scanner(System.in);
//获取用户输入的家电编号
String index = sc.next();
//将用户输入的内容作为数组索引执行对应家电开关的方法
switch (index){
case "1":
hc.switchJD(jds[0]);
break;
case "2":
hc.switchJD(jds[1]);
break;
case "3":
hc.switchJD(jds[2]);
break;
//如果输入exit则结束循环
case "exit":
System.out.println("结束操作,已退出!");
return;
//其他情况则提示输入错误
default:
System.out.println("输入错误!");
break;
}
}
}
}