Java代码审计-SE-4
构造器
接下来我们学习一个非常实用的语法知识——叫做构造器。
关于构造器,我们掌握下面几个问题就可以了:
- 什么是构造器?
- 掌握构造器的特点?
- 构造器的应用场景?
- 构造器有哪些注意事项?
我们一个问题一个问题的来学习,先来学习什么是构造器?
- 什么是构造器?
构造器其实是一种特殊的方法,但是这个方法没有返回值类型,方法名必须和类名相同。
如下图所示:下面有一个Student类,构造器名称也必须叫Student;也有空参数构造器,也可以有有参数构造器。
认识了构造器之后,接着我们看一下构造器有什么特点。
- 构造器的特点?
在创建对象时,会调用构造器。
也就是说 new Student()
就是在执行构造器,当构造器执行完了,也就意味着对象创建成功。
当执行new Student("播仔",99)
创建对象时,就是在执行有参数构造器,当有参数构造器执行完,就意味着对象创建完毕了。
关于构造器的特点,我们记住一句话:new 对象就是在执行构造方法
- 构造器的应用场景?
其实构造器就是用来创建对象的。可以在创建对象时给对象的属性做一些初始化操作。如下图所示:
- 构造器的注意事项?
学习完构造器的应用场景之后,接下来我们再看一下构造器有哪些注意事项。
1.在设计一个类时,如果不写构造器,Java会自动生成一个无参数构造器。
2.一旦定义了有参数构造器,Java就不再提供空参数构造器,此时建议自己加一个无参数构造器。
关于构造器的这几个问题我们再总结一下。掌握这几个问题,构造方法就算完全明白了。
1.什么是构造器?答:构造器其实是一种特殊的方法,但是这个方法没有返回值类型,方法名必须和类名相 同。2.构造器什么时候执行?答:new 对象就是在执行构造方法;3.构造方法的应用场景是什么?答:在创建对象时,可以用构造方法给成员变量赋值4.构造方法有哪些注意事项?1)在设计一个类时,如果不写构造器,Java会自动生成一个无参数构造器。2)一定定义了有参数构造器,Java就不再提供空参数构造器,此时建议自己加一个无参数构 造器。
封装性
各位同学,接下来我们再学习一个面向对象很重要的特征叫做——封装性。
1. 什么是封装呢?
所谓封装,就是用类设计对象处理某一个事物的数据时,应该把要处理的数据,以及处理数据的方法,都设计到一个对象中去。
比如:在设计学生类时,把学生对象的姓名、语文成绩、数学成绩三个属性,以及求学生总分、平均分的方法,都封装到学生对象中来。
现在我们已经知道什么是封装了。那我们学习封装,学习个啥呢? 其实在实际开发中,在用类设计对事处理的数据,以及对数据处理的方法时,是有一些设计规范的。
封装的设计规范用8个字总结,就是:合理隐藏、合理暴露
比如,设计一辆汽车时,汽车的发动机、变速箱等一些零件并不需要让每一个开车的知道,所以就把它们隐藏到了汽车的内部。
把发动机、变速箱等这些零件隐藏起来,这样做其实更加安全,因为并不是所有人都很懂发动机、变速箱,如果暴露在外面很可能会被不懂的人弄坏。
在设计汽车时,除了隐藏部分零件,但是还是得合理的暴露一些东西出来,让司机能够操纵汽车,让汽车跑起来。比如:点火按钮啊、方向盘啊、刹车啊、油门啊、档把啊... 这些就是故意暴露出来让司机操纵汽车的。
好了,到现在我们已经理解什么是封装的一些规范了。就是:合理暴露、合理隐藏
2. 封装在代码中的体现
知道什么是封装之后,那封装在代码中如何体现呢?一般我们在设计一个类时,会将成员变量隐藏,然后把操作成员变量的方法对外暴露。
这里需要用到一个修饰符,叫private,被private修饰的变量或者方法,只能在本类中被访问。
如下图所示,private double score;
就相当于把score变量封装在了Student对象的内部,且不对外暴露,你想要在其他类中访问score这个变量就,就不能直接访问了;
如果你想给Student对象的score属性赋值,得调用对外暴露的方法setScore(int score)
,在这个方法中可以对调用者传递过来的数据进行一些控制,更加安全。
当你想获取socre变量的值时,就得调用对外暴露的另一个方法 getScore()
Eg:
public class StudentInfo { private String idCard;public void setIdCard(String idCard) {this.idCard = idCard;}public void getIdCard() {System.out.println(this.idCard);}
}
public class ObjectOrientedDemo {public static void main(String[] args) {StudentInfo si = new StudentInfo();si.setIdCard("340421200002022020");si.getIdCard();}
}
关于封装我们就学习到这里了。
实体JavaBean
接下来,我们学习一个面向对象编程中,经常写的一种类——叫实体JavaBean类。我们先来看什么是实体类?
1. 什么是实体类?
实体类就是一种特殊的类,它需要满足下面的要求:
接下来我们按照要求,写一个Student实体类;
写完实体类之后,我们看一看它有什么特点? 其实我们会发现实体类中除了有给对象存、取值的方法就没有提供其他方法了。所以实体类仅仅只是用来封装数据用的。
Java Bean类
package com.learn.test.bean;public class Student {private String name;private double score;public Student(){}public Student(String name, double score) {this.name = name;this.score = score;}public String getName() {return name;}public void setName(String name) {this.name = name;}public double getScore() {return score;}public void setScore(double score) {this.score = score;}
}
package com.learn.test.bean;/*
* java bean 类要求类中的成员变量都要私有,并且要对外提供响应的getXXX,setXXX的方法
* java bean 类要求类中必须要有一个公共无参的构造器
* */
public class JavaBeanTest {public static void main(String[] args) {Student s1 = new Student("Fengling", 100);System.out.println(s1.getName());System.out.println(s1.getScore());s1.setName("John");s1.setScore(80);System.out.println(s1.getName());System.out.println(s1.getScore());}
}
知道实体类有什么特点之后,接着我们看一下它有哪些应用场景?
实体类的应用场景
在实际开发中,实体类仅仅只用来封装数据,而对数据的处理交给其他类来完成,以实现数据和数据业务处理相分离。如下图所示
在实际应用中,会将类作为一种数据类型使用。如下图所示,在StudentOperator类中,定义一个Student类型的成员变量student,然后使用构造器给student成员变量赋值。
然后在Student的printPass()方法中,使用student调用Student对象的方法,对Student对象的数据进行处理。
到这里,我们已经学习了JavaBean实体类的是什么,以及它的应用场景,我们总结一下
1.JavaBean实体类是什么?有啥特点JavaBean实体类,是一种特殊的;它需要私有化成员变量,有空参数构造方法、同时提供getXxx和setXxx方法;JavaBean实体类仅仅只用来封装数据,只提供对数据进行存和取的方法2.JavaBean的应用场景?JavaBean实体类,只负责封装数据,而把数据处理的操作放在其他类中,以实现数据和数据处理相分离。
面向对象综合案例
学习完面向对象的语法知识之后。接下来,我们做一个面向对象的综合案例——模仿电影信息系统。
需求如下图所示
1. 想要展示系统中全部的电影信息(每部电影:编号、名称、价格)
2. 允许用户根据电影的编号(id),查询出某个电影的详细信息。
运行程序时,能够根据用户的选择,执行不同的功能,如下图所示
自己写的代码:
需求分析:展示所有的电影:前提1,电影类-java bean=>数据+Get\Set+无参的构造器 2.电影操作器(类) 3.测试类=>电影对象数组(一部电影就是一个对象)、简陋的操作界面+接收用户输入+对用户输入进行反应
按照下面的步骤来完成需求
首先每一部电影,都包含这部电影的相关信息,比如:电影的编号(id)、电影的名称(name)、电影的价格(price)、电影的分数(score)、电影的导演(director)、电影的主演(actor)、电影的简介(info)。
为了去描述每一部电影,有哪些信息,我们可以设计一个电影类(Movie),电影类仅仅只是为了封装电影的信息,所以按照JavaBean类的标准写法来写就行。
public class Movie {private int id;private String name;private double price;private double score;private String director;private String actor;private String info;public Movie() {}public Movie(int id, String name, double price, double score, String director, String actor, String info) {this.id = id;this.name = name;this.price = price;this.score = score;this.director = director;this.actor = actor;this.info = info;}public int getId() {return id;}public void setId(int id) {this.id = id;}public String getName() {return name;}public void setName(String name) {this.name = name;}public double getPrice() {return price;}public void setPrice(double price) {this.price = price;}public double getScore() {return score;}public void setScore(double score) {this.score = score;}public String getDirector() {return director;}public void setDirector(String director) {this.director = director;}public String getActor() {return actor;}public void setActor(String actor) {this.actor = actor;}public String getInfo() {return info;}public void setInfo(String info) {this.info = info;}
}
第二步:定义电影操作类
前面我们定义的Movie类,仅仅只是用来封装每一部电影的信息。为了让电影数据和电影数据的操作相分离,我们还得有一个电影操作类(MovieOperator)。
因为系统中有多部电影,所以电影操作类中MovieOperator,需要有一个Movie[] movies;
用来存储多部电影对象;
同时在MovieOperator类中,提供对外提供,对电影数组进行操作的方法。如printAllMovies()
用于打印数组中所有的电影信息,searchMovieById(int id)
方法根据id查找一个电影的信息并打印。
public class MovieOperator {//因为系统中有多部电影,所以电影操作类中,需要有一个Movie的数组private Movie[] movies;public MovieOperator(Movie[] movies){this.movies = movies;}/** 1、展示系统全部电影信息 movies = [m1, m2, m3, ...]*/public void printAllMovies(){System.out.println("-----系统全部电影信息如下:-------");for (int i = 0; i < movies.length; i++) {Movie m = movies[i];System.out.println("编号:" + m.getId());System.out.println("名称:" + m.getName());System.out.println("价格:" + m.getPrice());System.out.println("------------------------");}}/** 2、根据电影的编号查询出该电影的详细信息并展示 */public void searchMovieById(int id){for (int i = 0; i < movies.length; i++) {Movie m = movies[i];if(m.getId() == id){System.out.println("该电影详情如下:");System.out.println("编号:" + m.getId());System.out.println("名称:" + m.getName());System.out.println("价格:" + m.getPrice());System.out.println("得分:" + m.getScore());System.out.println("导演:" + m.getDirector());System.out.println("主演:" + m.getActor());System.out.println("其他信息:" + m.getInfo());return; // 已经找到了电影信息,没有必要再执行了}}System.out.println("没有该电影信息~");}
}
第三步:定义测试类
最后,我们需要在测试类中,准备好所有的电影数据,并用一个数组保存起来。每一部电影的数据可以封装成一个对象。然后把对象用数组存起来即可。
public class Test {public static void main(String[] args) {//创建一个Movie类型的数组Movie[] movies = new Movie[4];//创建4个电影对象,分别存储到movies数组中movies[0] = new Movie(1,"水门桥", 38.9, 9.8, "徐克", "吴京","12万人想看");movies[1] = new Movie(2, "出拳吧", 39, 7.8, "唐晓白", "田雨","3.5万人想看");movies[2] = new Movie(3,"月球陨落", 42, 7.9, "罗兰", "贝瑞","17.9万人想看");movies[3] = new Movie(4,"一点就到家", 35, 8.7, "许宏宇", "刘昊然","10.8万人想看");}
}
准备好测试数据之后,接下来就需要对电影数据进行操作。我们已经把对电影操作先关的功能写到了MovieOperator类中,所以接下来,创建MovieOperator类对象,调用方法就可以完成相关功能。
继续再main方法中,接着写下面的代码。
// 4、创建一个电影操作类的对象,接收电影数据,并对其进行业务处理
MovieOperator operator = new MovieOperator(movies);
Scanner sc = new Scanner(System.in);
while (true) {System.out.println("==电影信息系统==");System.out.println("1、查询全部电影信息");System.out.println("2、根据id查询某个电影的详细信息展示");System.out.println("请您输入操作命令:");int command = sc.nextInt();switch (command) {case 1:// 展示全部电影信息operator.printAllMovies();break;case 2:// 根据id查询某个电影的详细信息展示System.out.println("请您输入查询的电影id:");int id = sc.nextInt();operator.searchMovieById(id);break;default:System.out.println("您输入的命令有问题~~");}
}
到这里,电影信息系统就完成了。 小伙伴们,自己尝试写一下吧!!
成员变量和局部变量的区别
各位同学,面向对象的基础内容咱们已经学习完了。同学们在面向对象代码时,经常会把成员变量和局部变量搞混。所以现在我们讲一讲他们的区别。
如下图所示,成员变量在类中方法外,而局部变量在方法中。
到这里,我们关于面向对象的基础知识就学习完了。面向对象的核心点就是封装,将数据和数据的处理方式,都封装到对象中; 至于对象要封装哪些数据?对数据进行怎样的处理? 需要通过类来设计。
需要注意的是,不同的人,对同一个对象进行设计,对象封装那些数据,提供哪些方法,可能会有所不同;只要能够完成需求,符合设计规范,都是合理的设计。
Java - 面向对象高级
一、静态
接下来,我们学习一下面向对象编程中很常见的一个关键字static.
static读作静态,可以用来修饰成员变量,也能修饰成员方法。我们先来学习static修饰成员变量。
static修饰成员变量
Java中的成员变量按照有无static修饰分为两种:类变量、实例变量。它们的区别如下图所示:
static修饰成员变量
Java中的成员变量按照有无static修饰分为两种:类变量、实例变量。它们的区别如下图所示:
由于静态变量是属于类的,只需要通过类名就可以调用:类名.静态变量
实例变量是属于对象的,需要通过对象才能调用:对象.实例变量
- 下面是代码演示(注意静态变量,和实例变量是如何调用的)
为了让大家对于这两种成员变量的执行过程理解更清楚一点,在这里给大家在啰嗦几句,我们来看一下上面代码的内存原理。
- 最后总结一下
- 1.类变量:属于类,在内存中只有一份,用类名调用
- 2.实例变量:属于对象,每一个对象都有一份,用对象调用
static修饰成员变量的应用场景
学习完static修饰成员变量的基本使用之后,接下来我们学习一下static修饰成员变量在实际工作中的应用。
在实际开发中,如果某个数据只需要一份,且希望能够被共享(访问、修改),则该数据可以定义成类变量来记住。
我们看一个案例**
需求:系统启动后,要求用于类可以记住自己创建了多少个用户对象。**
- 第一步:先定义一个
User
类,在用户类中定义一个static修饰的变量,用来表示在线人数;
public class User{public static int number;//每次创建对象时,number自增一下public User(){User.number++;}
}
- 第二步:再写一个测试类,再测试类中创建4个User对象,再打印number的值,观察number的值是否再自增。
public class Test{public static void main(String[] args){//创建4个对象new User();new User();new User();new User(); //查看系统创建了多少个User对象System.out.println("系统创建的User对象个数:"+User.number);}
}
运行上面的代码,查看执行结果是:系统创建的User对象个数:4
static修饰成员方法
各位同学,学习完static修饰成员变量之后,接下来我们学习static修饰成员方法。成员方法根据有无static也分为两类:类方法、实例方法
有static修饰的方法,是属于类的,称为类方法;调用时直接用类名调用即可。
无static修饰的方法,是属于对象的,称为实例方法;调用时,需要使用对象调用。
我们看一个案例,演示类方法、实例方法的基本使用
- 先定义一个Student类,在类中定义一个类方法、定义一个实例方法
public class Student{double score;//类方法:public static void printHelloWorld{System.out.println("Hello World!");System.out.println("Hello World!");}//实例方法(对象的方法)public void printPass(){//打印成绩是否合格System.out.println(score>=60?"成绩合格":"成绩不合格");}
}
- 在定义一个测试类,注意类方法、对象方法调用的区别
public class Test2{public static void main(String[] args){//1.调用Student类中的类方法Student.printHelloWorld();//2.调用Student类中的实例方法Student s = new Student(); s.printPass();//使用对象也能调用类方法【不推荐,IDEA连提示都不给你,你就别这么用了】s.printHelloWorld();}
}
搞清楚类方法和实例方法如何调用之后,接下来再啰嗦几句,和同学们聊一聊static修饰成员方法的内存原理。
1.类方法:static修饰的方法,可以被类名调用,是因为它是随着类的加载而加载的;所以类名直接就可以找到static修饰的方法2.实例方法:非static修饰的方法,需要创建对象后才能调用,是因为实例方法中可能会访问实例变量,而实例变量需要创建对象后才存在。所以实例方法,必须创建对象后才能调用。
关于static修饰成员变量、和静态修饰成员方法这两种用法,到这里就学习完了。
工具类
工具类
学习完static修饰方法之后,我们讲一个有关类方法的应用知识,叫做工具类。
如果一个类中的方法全都是静态的,那么这个类中的方法就全都可以被类名直接调用,由于调用起来非常方便,就像一个工具一下,所以把这样的类就叫做工具类。
- 我们写一个生成验证码的工具类
public class MyUtils{public static String createCode(int n){//1.定义一个字符串,用来记录产生的验证码String code = "";//2.验证码是由所有的大写字母、小写字母或者数字字符组成//这里先把所有的字符写成一个字符串,一会从字符串中随机找字符String data = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKMNOPQRSTUVWXYZ";//3.循环n次,产生n个索引,再通过索引获取字符Random r = new Random();for(int i=0; i<n; i++){int index = r.nextInt(data.length());char ch = data.charAt(index);//4.把获取到的字符,拼接到code验证码字符串上。code+=ch;}//最后返回code,code的值就是验证码return code;}
}
- 接着可以在任何位置调用
MyUtils
的createCOde()方法
产生任意个数的验证码
//比如这是一个登录界面
public class LoginDemo{public static void main(String[] args){System.out.println(MyUtils.createCode());}
}
//比如这是一个注册界面
public class registerDemo{public static void main(String[] args){System.out.println(MyUtils.createCode());}
}
工具类的使用就是这样子的,学会了吗?
在补充一点,工具类里的方法全都是静态的,推荐用类名调用为了防止使用者用对象调用。我们可以把工具类的构造方法私有化。
public class MyUtils{//私有化构造方法:这样别人就不能使用构造方法new对象了private MyUtils(){}//类方法public static String createCode(int n){...}
}
static的注意事项
各位同学,到现在在我们已经学会了static修饰的变量、方法如何调用了。但是有一些注意事项还是需要给大家说明一下,目的是让大家知道,使用static写代码时,如果出错了,要知道为什么错、如何改正。
public class Student {static String schoolName; // 类变量double score; // 实例变量// 1、类方法中可以直接访问类的成员,不可以直接访问实例成员。public static void printHelloWorld(){// 注意:同一个类中,访问类成员,可以省略类名不写。schoolName = "FengLing";printHelloWorld2();System.out.println(score); // 报错的printPass(); // 报错的ystem.out.println(this); // 报错的}// 类方法public static void printHelloWorld2(){}// 实例方法public void printPass2(){}// 实例方法// 2、实例方法中既可以直接访问类成员,也可以直接访问实例成员。// 3、实例方法中可以出现this关键字,类方法中不可以出现this关键字的public void printPass(){schoolName = "FengLing2"; //对的printHelloWorld2(); //对的System.out.println(score); //对的printPass2(); //对的System.out.println(this); //对的}
}
static应用(代码块)
各位同学,接下来我们再补充讲解一个知识点,叫代码块;代码块根据有无static修饰分为两种:静态代码块、实例代码块
我们先类学习静态代码块:
public class StaticCode {static String schoolName;static String Name = "北京";static{System.out.println("我是一个静态代码块,我在" + Name);schoolName = "北京大学";}
}
静态代码块不需要创建对象就能够执行
package com.learn.test.oop;public class StaticCodeTest {public static void main(String[] args) {System.out.println(StaticCode.Name);System.out.println(StaticCode.schoolName);}
}
执行上面代码时,发现没有创建对象,静态代码块就已经执行了。
关于静态代码块重点注意:静态代码块,随着类的加载而执行,而且只执行一次。
实例代码块
再来学习一下实例代码块
实例代码块的作用和构造器的作用是一样的,用来给对象初始化值;而且每次创建对象之前都会先执行实例代码块。
public class Student{//实例变量int age;//实例代码块:实例代码块会执行在每一个构造方法之前{System.out.println("实例代码块执行了~~");age = 18;System.out.println("有人创建了对象:" + this);}public Student(){System.out.println("无参数构造器执行了~~");}public Student(String name){System.out.println("有参数构造器执行了~~");}
}
接下来在测试类中进行测试,观察创建对象时,实例代码块是否先执行了。
public class Test {public static void main(String[] args) {Student s1 = new Student();Student s2 = new Student("张三");System.out.println(s1.age);System.out.println(s2.age);}
}
对于实例代码块重点注意:实例代码块每次创建对象之前都会执行一次
再来学习一下实例代码块
实例代码块的作用和构造器的作用是一样的,用来给对象初始化值;而且每次创建对象之前都会先执行实例代码块。
public class InstanceCode {int age;{System.out.println("实例化代码块执行了");this.age = 20;System.out.println("有人创建了对象:" + this);}public InstanceCode() {System.out.println("无参数构造器执行了~~");}public InstanceCode(int age) {this.age = age;System.out.println("有参数构造器执行了~~");}
}
接下来在测试类中进行测试,观察创建对象时,实例代码块是否先执行了。
public class InstanceCodeTest {public static void main(String[] args) {InstanceCode ic1 = new InstanceCode();InstanceCode ic2 = new InstanceCode(10);}
}
对于实例代码块重点注意:实例代码块每次创建对象之前都会执行一次
单例设计模式
什么是设计模式?
所谓设计模式指的是,一类问题可能会有多种解决方案,而设计模式是在编程实践中,多种方案中的一种最优方案。
设计模式有几种?
根据《设计模式:可复用面向对象软件的基础》(GoF, Gang of Four)一书,设计模式通常分为三大类,共23种经典模式:
创建型模式(Creational Patterns) 关注对象的创建过程,优化内存分配和实例化方式。
- 示例:
- 单例模式(Singleton):确保类只有一个实例,提供全局访问点(如前文所述,静态变量存储在方法区/元空间)。
- 工厂方法(Factory Method):定义创建对象的接口,子类决定实例化哪个类。
- 抽象工厂(Abstract Factory):创建相关对象家族,无需指定具体类。
- 建造者(Builder):分离复杂对象的构建与表示。
- 原型(Prototype):通过克隆创建对象,减少内存分配开销。
结构型模式(Structural Patterns) 关注类和对象的组合,优化系统结构。
- 示例:
- 适配器(Adapter):将不兼容的接口转换为可用的接口。
- 装饰者(Decorator):动态为对象添加职责。
- 代理(Proxy):控制对对象的访问(如Java动态代理,涉及反射)。
- 外观(Facade):为复杂子系统提供简单接口。
- 组合(Composite):将对象组织成树形结构。
行为型模式(Behavioral Patterns) 关注对象之间的通信和职责分配。
- 示例:
- 观察者(Observer):定义一对多的依赖关系,对象状态变化时通知依赖者。
- 策略(Strategy):封装可互换的算法,动态选择行为。
- 模板方法(Template Method):定义算法骨架,子类实现具体步骤。
- 责任链(Chain of Responsibility):将请求沿处理链传递。
- 迭代器(Iterator):提供顺序访问集合元素的方法(如Java的 Iterator 接口)。
为什么要学习设计模式?
面试中常被问及、看懂源码需要、模式经过广泛验证,避免了新手常犯的设计错误。
Static应用:单例设计模式
单例设计模式的核心目标:保证一个类全局只有一个实例,防止多次实例化浪费资源或导致不一致状态。
它在Java中常用于需要统一资源管理或控制访问的场景,如配置管理、日志记录、数据库连接池等。单例设计模式有以下几点关键要素:
- 私有构造函数:防止外部通过 new 创建实例。
- 静态变量:存储唯一实例,通常位于JVM的方法区/元空间。
- 静态方法:提供全局访问点(如 getInstance())。
主要实现方式
①饿汉式单例模式:类加载时立即创建实例,线程安全。
内存分析:静态变量在类加载的初始化阶段分配内存,存储在方法区/元空间,实例在堆中创建。
实现方式:
①必须私有化类的构造器
②定义一个类的静态变量存储的唯一一个实例
③提供一个全局访问方法(如:getInstance),通过全局访问方法获取类的唯一示例。
public class Singleton {// 静态变量存储唯一实例private static final Singleton instance = new Singleton();// 私有构造函数private Singleton() {}// 全局访问点public static Singleton getInstance() {return instance;}
}
单例模式对象的获取
public class SingleInstanceTest {public static void main(String[] args) {SingleInstance si1 = SingleInstance.getInstance();SingleInstance si2 = SingleInstance.getInstance();//判断这两个对象地址是否相等(是否为同一对象)System.out.println(si1 == si2);}
}
执行结果为true:说明实例化的这两个对象实际上是同一个对象。
②懒汉式单例模式:延迟加载,首次调用 getInstance() 时创建实例。
内存分析:实例在堆中动态分配,初始时静态变量为 null,节省内存直到需要时。
public class Singleton {private static Singleton instance;private Singleton() {}public static Singleton getInstance() {if (instance == null) { // 条件代码块instance = new Singleton();}return instance;}
}
缺点:非线程安全,多线程下可能创建多个实例。
继承
继承快速入门
各位同学,我们继续学习面向对象相关内容。面向对象编程之所以能够能够被广大开发者认可,有一个非常重要的原因,是因为它有三大特征,继承、封装和多态。封装我们在基础班已经学过了,接下来我们学习一下继承。
接下来,我们演示一下使用继承来编写代码,注意观察继承的特点。
public class A{//公开的成员public int i;public void print1(){System.out.println("===print1===");}//私有的成员private int j;private void print2(){System.out.println("===print2===");}
}
然后,写一个B类,让B类继承A类。在继承A类的同时,B类中新增一个方法print3
public class B extends A{public void print3(){//由于i和print1是属于父类A的公有成员,在子类中可以直接被使用System.out.println(i); //正确print1(); //正确//由于j和print2是属于父类A的私有成员,在子类中不可以被使用System.out.println(j); //错误print2();}
}
接下来,我们再演示一下,创建B类对象,能否调用父类A的成员。再写一个测试类
public class Test{public static void main(String[] args){B b = new B();//父类公有成员,子类对象是可以调用的System.out.println(i); //正确b.print1();//父类私有成员,子类对象时不可以调用的System.out.println(j); //错误b.print2(); //错误}
}
到这里,关于继承的基本使用我们就算学会了。为了让大家对继承有更深入的认识,我们来看看继承的内存原理。这里我们只需要关注一点:子类对象实际上是由子、父类两张设计图共同创建出来的。
所以,在子类对象的空间中,既有本类的成员,也有父类的成员。但是子类只能调用父类公有的成员。
当JVM加载类时(通过类加载器),父类和子类的类元数据分别加载到方法区/元空间。
父类和子类的类元数据(Class Metadata)存储在方法区/元空间,包括类结构、字段描述、方法字节码和方法表子类的元数据包含对父类的引用,形成继承链。类加载时,父类元数据先分配,子类随后分配。
继承的好处
各位同学,学习完继承的快速入门之后,接下来我们学习一下继承的好处,以及它的应用场景。
我们通过一个案例来学习
观察代码发现,我们会发现Teacher类中和Consultant类中有相同的代码;其实像这种两个类中有相同代码时,没必要重复写。
我们可以把重复的代码提取出来,作为父类,然后让其他类继承父类就可以了,这样可以提高代码的复用性。改造后的代码如下:
接下来使用继承来完成上面的案例,这里只演示People类和Teacher类,然后你尝试自己完成Consultant类。
- 先写一个父类 People,用来设计Teacher和Consultant公有的成员。
public class People{private String name;public String getName(){return name;}public void setName(String name){this.name=name;}
}
- 再写两个子类Teacher继承People类,同时在子类中加上自己特有的成员。
public class Teacher extends People{private String skill; //技能public String getSkill(){return skill;}public void setSkill(String skill){this.skill=skill;}public void printInfo(){System.out.println(getName()+"具备的技能:"+skill);}
}
- 最后再写一个测试类,再测试类中创建Teacher、Consultant对象,并调用方法。
public class Test {public static void main(String[] args) {// 目标:搞清楚继承的好处。Teacher t = new Teacher();t.setName("播仔");t.setSkill("Java、Spring");System.out.println(t.getName());System.out.println(t.getSkill());t.printInfo();}
}
执行代码,打印结果如下:
关于继承的好处我们只需要记住:继承可以提高代码的复用性
权限修饰符
各位同学,在刚才使用继承编写的代码中我们有用到两个权限修饰符,一个是public(公有的)、一个是private(私有的),实际上还有两个权限修饰符,一个是protected(受保护的)、一个是缺省的(不写任何修饰符)。
接下来我们就学习一下这四个权限修饰符分别有什么作用。
什么是权限修饰符呢?
权限修饰符是用来限制类的成员(成员变量、成员方法、构造器...)能够被访问的范围。
每一种权限修饰符能够被访问的范围如下
下面我们用代码演示一下,在本类中可以访问到哪些权限修饰的方法。
public class Fu {// 1、私有:只能在本类中访问private void privateMethod(){System.out.println("==private==");}// 2、缺省:本类,同一个包下的类void method(){System.out.println("==缺省==");}// 3、protected: 本类,同一个包下的类,任意包下的子类protected void protectedMethod(){System.out.println("==protected==");}// 4、public: 本类,同一个包下的类,任意包下的子类,任意包下的任意类public void publicMethod(){System.out.println("==public==");}public void test(){//在本类中,所有权限都可以被访问到privateMethod(); //正确method(); //正确protectedMethod(); //正确publicMethod(); //正确}
}
接下来,在和Fu类同一个包下,创建一个测试类Demo,演示同一个包下可以访问到哪些权限修饰的方法。
public class Demo {public static void main(String[] args) {Fu f = new Fu();// f.privateMethod(); //私有方法无法使用f.method();f.protectedMethod();f.publicMethod();}
}
接下来,在另一个包下创建一个Fu类的子类,演示不同包下的子类中可以访问哪些权限修饰的方法。
public class Zi extends Fu {//在不同包下的子类中,只能访问到public、protected修饰的方法public void test(){// privateMethod(); // 报错// method(); // 报错protectedMethod(); //正确publicMethod(); //正确}
}
接下来,在和Fu类不同的包下,创建一个测试类Demo2,演示一下不同包的无关类,能访问到哪些权限修饰的方法;
public class Demo2 {public static void main(String[] args) {Fu f = new Fu();// f.privateMethod(); // 报错// f.method(); //报错// f.protecedMethod();//报错f.publicMethod(); //正确Zi zi = new Zi();// zi.protectedMethod();}
}
单继承、Object
刚才我们写的代码中,都是一个子类继承一个父类,那么有同学问到,一个子类可以继承多个父类吗?
Java语言只支持单继承,不支持多继承,但是可以多层继承。就像家族里儿子、爸爸和爷爷的关系一样:一个儿子只能有一个爸爸,不能有多个爸爸,但是爸爸也是有爸爸的。
方法重写
各位同学,学习完继承之后,在继承的基础之上还有一个很重要的现象需要给大家说一下。
叫做方法重写。为了让大家能够掌握方法重写,我们先认识什么是方法重写,再说一下方法的应用场景。
什么是方法重写
当子类觉得父类方法不好用,或者无法满足父类需求时,子类可以重写一个方法名称、参数列表一样的方法,去覆盖父类的这个方法,这就是方法重写。
注意:重写后,方法的访问遵循就近原则。下面我们看一个代码演示
写一个A类作为父类,定义两个方法print1和print2
public class A {public void print1(){System.out.println("111");}public void print2(int a, int b){System.out.println("111111");}
}
再写一个B类作为A类的子类,重写print1和print2方法。
public class B extends A{// 方法重写@Override // 安全,可读性好public void print1(){System.out.println("666");}// 方法重写@Overridepublic void print2(int a, int b){System.out.println("666666");}
}
接下来,在测试类中创建B类对象,调用方法
public class Test {public static void main(String[] args) {// 目标:认识方法重写,掌握方法重写的常见应用场景。B b = new B();b.print1();b.print2(2, 3);}
}
执行代码,我们发现真正执行的是B类中的print1和print2方法
知道什么是方法重写之后,还有一些注意事项,需要和大家分享一下。
- 1.重写的方法上面,可以加一个注解@Override,用于标注这个方法是复写的父类方法
- 2.子类复写父类方法时,访问权限必须大于或者等于父类方法的权限public > protected > 缺省
- 3. 重写的方法返回值类型,必须与被重写的方法返回值类型一样,或者范围更小
- 4. 私有方法、静态方法不能被重写,如果重写会报错。
关于这些注意事项,其实只需要了解一下就可以了。实际上我们实际写代码时,只要和父类写的一样就可以( 总结起来就8个字:声明不变,重新实现)
**方法重写的应用场景**
学习完方法重写之后,接下来,我们还需要大家掌握方法重写,在实际中的应用场景。方法重写的应用场景之一就是:子类重写Object的toString()方法,以便返回对象的内容。
比如:有一个Student类,这个类会默认继承Object类。
public class Student extends Object{private String name;private int age;public Student() {}public Student(String name, int age) {this.name = name;this.age = age;}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;}
}
其实Object类中有一个toString()方法,直接通过Student对象调用Object的toString()方法,会得到对象的地址值。
public class Test {public static void main(String[] args) {Student s = new Student("FengLing", 19);// System.out.println(s.toString());System.out.println(s);}
}
但是,此时不想调用父类Object的toString()方法,那就可以在Student类中重新写一个toSting()方法,用于返回对象的属性值。
package com.itheima.d12_extends_override;public class Student extends Object{private String name;private int age;public Student() {}public Student(String name, int age) {this.name = name;this.age = age;}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;}@Overridepublic String toString() {return "Student{" +"name='" + name + '\'' +", age=" + age +'}';}
}
重新运行测试类,结果如下
好了,到这里方法什么是方法重写,以及方法重写的应用场景我们就学习完了。
子类中访问成员的特点
各位同学,刚才我们已经学习了继承,我们发现继承至少涉及到两个类,而每一个类中都可能有各自的成员(成员变量、成员方法),就有可能出现子类和父类有相同成员的情况,那么在子类中访问其成员有什么特点呢?
- 原则:在子类中访问他成员(成员变量、成员方法),是依据就近原则
定义一个父类,代码如下
public class F {String name = "父类名字";public void print1(){System.out.println("==父类的print1方法执行==");}
}
再定义一个子类,代码如下。有一个同名的name成员变量,有一个同名的print1成员方法;
public class Z extends F {String name = "子类名称";public void showName(){String name = "局部名称";System.out.println(name); // 局部名称}@Overridepublic void print1(){System.out.println("==子类的print1方法执行了=");}public void showMethod(){print1(); // 子类的}
}
接下来写一个测试类,观察运行结果,我们发现都是调用的子类变量、子类方法。
public class Test {public static void main(String[] args) {// 目标:掌握子类中访问其他成员的特点:就近原则。Z z = new Z();z.showName(); //打印了局部变量z.showMethod(); //执行了子类的方法}
}
- 如果子类和父类出现同名变量或者方法,优先使用子类的;此时如果一定要在子类中使用父类的成员,可以加this或者super进行区分。
public class Z extends F {String name = "子类名称";public void showName(){String name = "局部名称";System.out.println(name); // 局部名称System.out.println(this.name); // 子类成员变量System.out.println(super.name); // 父类的成员变量}@Overridepublic void print1(){System.out.println("==子类的print1方法执行了=");}public void showMethod(){print1(); // 子类的super.print1(); // 父类的}
}
测试类:
public class Test {public static void main(String[] args) {Z z = new Z();z.showName();z.showMethod();}
}
子类中访问构造器的特点
各位同学,我们知道一个类中可以写成员变量、成员方法,还有构造器。在继承关系下,子类访问成员变量和成员方法的特点我们已经学过了;接下来再学习子类中访问构造器的特点。
我们先认识子类构造器的语法特点,再讲一下子类构造器的应用场景
子类中访问构造器的语法规则
- 首先,子类全部构造器,都会先调用父类构造器,再执行自己。
执行顺序,如下图按照① ② ③ 步骤执行:
子类访问构造器的应用场景
- 如果不想使用默认的
super()
方式调用父类构造器,还可以手动使用super(参数)
调用父类有参数构造器。
在本类中访问自己的构造方法
刚才我们学习了通过super()
和super(参数)
可以访问父类的构造器。有时候我们也需要访问自己类的构造器。语法如下
this(): 调用本类的空参数构造器
this(参数): 调用本类有参数的构造器
最后我们被this和super的用法在总结一下
访问本类成员:this.成员变量 //访问本类成员变量this.成员方法 //调用本类成员方法this() //调用本类空参数构造器this(参数) //调用本类有参数构造器访问父类成员:super.成员变量 //访问父类成员变量super.成员方法 //调用父类成员方法super() //调用父类空参数构造器super(参数) //调用父类有参数构造器注意:this和super访问构造方法,只能用到构造方法的第一句,否则会报错。
多态
接下来,我们学习面向对象三大特征的的最后一个特征——多态。
多态概多态概述
**什么是多态?**
多态是在继承、实现情况下的一种现象,表现为:对象多态、行为多态。
比如:Teacher和Student都是People的子类,代码可以写成下面的样子
例子:
People
public class People {private int age;public void run(){System.out.println("People run");}
}
Student
public class Student extends People{public void run(){System.out.println("Student run");}
}
Teacher
public class Teacher extends People{public void run(){System.out.println("Teacher run");}
}
Test
public class Test {public static void main(String[] args) {People p1 = new Student();p1.run();People p2 = new Teacher();p2.run();}
}
多态的好处
各位同学,刚才我们认识了什么是多态。那么多态的写法有什么好处呢?
在多态形式下,右边的代码是解耦合的,更便于扩展和维护。
- 怎么理解这句话呢?比如刚开始p1指向Student对象,run方法执行的就是Student对象的业务;
定义方法时,使用父类类型作为形参,可以接收一切子类对象,扩展行更强,更便利。
public class Test2 {public static void main(String[] args) {// 目标:掌握使用多态的好处Teacher t = new Teacher();go(t);Student s = new Student();go(s);}//参数People p既可以接收Student对象,也能接收Teacher对象。public static void go(People p){System.out.println("开始------------------------");p.run();System.out.println("结束------------------------");}
}
类型转换
虽然多态形式下有一些好处,但是也有一些弊端。在多态形式下,不能调用子类特有的方法,比如在Teacher类中多了一个teach方法,在Student类中多了一个study方法,这两个方法在多态形式下是不能直接调用的。
多态形式下不能直接调用子类特有方法,但是转型后是可以调用的。这里所说的转型就是把父类变量转换为子类类型。格式如下:
//如果p接收的是子类对象
if(父类变量 instance 子类){//则可以将p转换为子类类型子类 变量名 = (子类)父类变量;
}
如果类型转换错了,就会出现类型转换异常ClassCastException,比如把Teacher类型转换成了Student类型.
关于多态转型问题,我们最终记住一句话:原本是什么类型,才能还原成什么类型
类型转换
虽然多态形式下有一些好处,但是也有一些弊端。在多态形式下,不能调用子类特有的方法,比如在Teacher类中多了一个teach方法,在Student类中多了一个study方法,这两个方法在多态形式下是不能直接调用的。
多态形式下不能直接调用子类特有方法,但是转型后是可以调用的。这里所说的转型就是把父类变量转换为子类类型。格式如下:
//如果p接收的是子类对象
if(父类变量 instance 子类){//则可以将p转换为子类类型子类 变量名 = (子类)父类变量;
}
如果类型转换错了,就会出现类型转换异常ClassCastException,比如把Teacher类型转换成了Student类型.
关于多态转型问题,我们最终记住一句话:原本是什么类型,才能还原成什么类型
案例代码:
People
public class People {private int age;public void run(){System.out.println("People run");}
}
Teacher
public class Teacher extends People{public void run(){System.out.println("Teacher run");}public void teach(){System.out.println("Teacher teach");}
}
Student
public class Student extends People{public void run(){System.out.println("Student run");}public void study(){System.out.println("Student study");}
}
Test
public class Test {public static void main(String[] args) {People p1 = new Student();//对象多态的情况下要调用子类方法需要进行类型转换if (p1 instanceof Student){Student s = (Student) p1;s.study();}People p2 = new Teacher();if(p2 instanceof Teacher){Teacher t = (Teacher) p2;t.teach();}}
}
final关键字
各位同学,接下来我们学习一个在面向对象编程中偶尔会用到的一个关键字叫final,也是为后面学习抽象类和接口做准备的。
final修饰符的特点
我们先来认识一下final的特点,final关键字是最终的意思,可以修饰类、修饰方法、修饰变量。
- final修饰类:该类称为最终类,特点是不能被继承
- final修饰方法:该方法称之为最终方法,特点是不能被重写。
- final修饰变量:该变量只能被赋值一次。
- 接下来我们分别演示一下,先看final修饰类的特点
- 再来演示一下final修饰方法的特点
- 再演示一下final修饰变量的特点
- 情况一
- 情况二
- 情况三
补充知识:常量
刚刚我们学习了final修饰符的特点,在实际运用当中经常使用final来定义常量。先说一下什么是Java中的常量?
- 被 static final 修饰的成员变量,称之为常量。
- 通常用于记录系统的配置信息
接下来我们用代码来演示一下
public class SystemConfig {//为了方便在其他类中被访问所以一般还会加上public修饰符//常量命名规范:建议都采用大写字母命名,多个单词之前有_隔开public static final String DB_NAME = "DVWA";public static final String DB_USER = "root";
}
public class FinalTest {public static void main(String[] args) {//由于常量是static的所以,在使用时直接用类名就可以调用System.out.println(SystemConfig.DB_NAME);System.out.println(SystemConfig.DB_USER);}
}
- 关于常量的原理,同学们也可以了解一下:在程序编译后,常量会“宏替换”,出现常量的地方,全都会被替换为其记住的字面量。把代码反编译后,其实代码是下面的样子
public class FinalTest {public static void main(String[] args) {//由于常量是static的所以,在使用时直接用类名就可以调用System.out.println("DVWA");System.out.println("DVWA");}
}
抽象
同学们,接下来我们学习Java中一种特殊的类,叫抽象类。为了让同学们掌握抽象类,会先让同学们认识一下什么是抽象类以及抽象类的特点,再学习一个抽象类的常见应用场景。
认识抽象类
我们先来认识一下什么是抽象类,以及抽象类有什么特点。
- 在Java中有一个关键字叫abstract,它就是抽象的意思,它可以修饰类也可以修饰方法。
- 被abstract修饰的类,就是抽象类
- 被abstract修饰的方法,就是抽象方法(不允许有方法体)
抽象方法(abstract method)只能在抽象类(abstract class)或接口(interface)中定义,不能在普通的非抽象类中定义。
接下来用代码来演示一下抽象类和抽象方法
//abstract修饰类,这个类就是抽象类
public abstract class A{//abstract修饰方法,这个方法就是抽象方法public abstract void test();
}
- 类的成员(成员变量、成员方法、构造器),类的成员都可以有。如下面代码
// 抽象类
public abstract class A {//成员变量private String name;static String schoolName;//构造方法public A(){}//抽象方法public abstract void test();//实例方法public String getName() {return name;}public void setName(String name) {this.name = name;}
}
- 抽象类是不能创建对象的,如果抽象类的对象就会报错
- 抽象类虽然不能创建对象,但是它可以作为父类让子类继承。而且子类继承父类必须重写父类的所有抽象方法。
//B类继承A类,必须复写test方法
public class B extends A {@Overridepublic void test() {}
}
- 子类继承父类如果不复写父类的抽象方法,要想不出错,这个子类也必须是抽象类
//B类基础A类,此时B类也是抽象类,这个时候就可以不重写A类的抽象方法
public abstract class B extends A {}
抽象类的好处
接下来我们用一个案例来说一下抽象类的应用场景和好处。需求如下图所示
分析需求发现,该案例中猫和狗都有名字这个属性,也都有叫这个行为,所以我们可以将共性的内容抽取成一个父类,Animal类,但是由于猫和狗叫的声音不一样,于是我们在Animal类中将叫的行为写成抽象的。代码如下
public abstract class Animal {private String name;//动物叫的行为:不具体,是抽象的public abstract void cry();public String getName() {return name;}public void setName(String name) {this.name = name;}
}
接着写一个Animal的子类,Dog类。代码如下
public class Dog extends Animal{public void cry(){System.out.println(getName() + "汪汪汪的叫~~");}
}
然后,再写一个Animal的子类,Cat类。代码如下
public class Cat extends Animal{public void cry(){System.out.println(getName() + "喵喵喵的叫~~");}
}
最后,再写一个测试类,Test类。
public class Test2 {public static void main(String[] args) {// 目标:掌握抽象类的使用场景和好处.Animal a = new Dog();a.cry(); //这时执行的是Dog类的cry方法}
}
再学一招,假设现在系统有需要加一个Pig类,也有叫的行为,这时候也很容易原有功能扩展。只需要让Pig类继承Animal,复写cry方法就行。
public class Pig extends Animal{@Overridepublic void cry() {System.out.println(getName() + "嚯嚯嚯~~~");}
}
此时,创建对象时,让Animal接收Pig,就可以执行Pig的cry方法
public class Test2 {public static void main(String[] args) {// 目标:掌握抽象类的使用场景和好处.Animal a = new Pig();a.cry(); //这时执行的是Pig类的cry方法}
}
综上所述,我们总结一下抽象类的使用场景和好处
1.用抽象类可以把父类中相同的代码,包括方法声明都抽取到父类,这样能更好的支持多态,一提高代码的灵活性。2.反过来用,我们不知道系统未来具体的业务实现时,我们可以先定义抽象类,将来让子类去实现,以方便系统的扩展。
模板方法设计模式(抽象类应用)
学习完抽象类的语法之后,接下来,我们学习一种利用抽象类实现的一种设计模式。先回顾一下什么是设计模式?设计模式是解决某一类问题的最优方案。
那模板方法设计模式解决什么问题呢?模板方法模式主要解决方法中存在重复代码的问题
比如A类和B类都有sing()方法,sing()方法的开头和结尾都是一样的,只是中间一段内容不一样。此时A类和B类的sing()方法中就存在一些相同的代码。
怎么解决上面的重复代码问题呢? 我们可以写一个抽象类C类,在C类中写一个doSing()的抽象方法。再写一个sing()方法,代码如下:
// 模板方法设计模式
public abstract class C {// 模板方法public final void sing(){System.out.println("唱一首你喜欢的歌:");doSing();System.out.println("唱完了!");}public abstract void doSing();
}
然后,写一个A类继承C类,复写doSing()方法,代码如下
public class A extends C{@Overridepublic void doSing() {System.out.println("我是一只小小小小鸟,想要飞就能飞的高~~~");}
}
接着,再写一个B类继承C类,也复写doSing()方法,代码如下
public class B extends C{@Overridepublic void doSing() {System.out.println("我们一起学猫叫,喵喵喵喵喵喵喵~~");}
}
最后,再写一个测试类Test
public class Test {public static void main(String[] args) {// 目标:搞清楚模板方法设计模式能解决什么问题,以及怎么写。B b = new B();b.sing();}
}
综上所述:模板方法模式解决了多个子类中有相同代码的问题。具体实现步骤如下
第1步:定义一个抽象类,把子类中相同的代码写成一个模板方法。
第2步:把模板方法中不能确定的代码写成抽象方法,并在模板方法中调用。
第3步:子类继承抽象类,只需要父类抽象方法就可以了。
接口
同学们,接下来我们学习一个比抽象类抽象得更加彻底的一种特殊结构,叫做接口。在学习接口是什么之前,有一些事情需要给大家交代一下:Java已经发展了30年了,在发展的过程中不同JDK版本的接口也有一些变化,所以我们在学习接口时,先以老版本为基础,学习完老版本接口的特性之后,再顺带着了解一些新版本接口的特性就可以了。
认识接口
我们先来认识一下接口?Java提供了一个关键字interface,用这个关键字来定义接口这种特殊结构。格式如下
public interface 接口名{//成员变量(常量)//成员方法(抽象方法)
}
按照接口的格式,我们定义一个接口看看
public interface A{//这里public static final可以加,可以不加。public static final String SCHOOL_NAME = "FengLing";//这里的public abstract可以加,可以不加。public abstract void test();
}
写好A接口之后,在写一个测试类,用一下
public class Test{public static void main(String[] args){//打印A接口中的常量System.out.println(A.SCHOOL_NAME);//接口是不能创建对象的A a = new A();}
}
我们发现定义好接口之后,类中的常量可以正常书删除,但是接口是不能创建对象的。
那接口到底什么使用呢?需要我注意下面两点
- 接口是用来被类实现(implements)的,我们称之为实现类。
- 一个类是可以实现多个接口的(接口可以理解成干爹),类实现接口必须重写所有接口的全部抽象方法,否则这个类也必须是抽象类
比如,再定义一个B接口,里面有两个方法testb1(),testb2()
public interface B {void testb1();void testb2();
}
接着,再定义一个C接口,里面有两个方法testc1(), testc2()
public interface C {void testc1();void testc2();
}
然后,再写一个实现类D,同时实现B接口和C接口,此时就需要复写四个方法,如下代码
// 实现类
public class D implements B, C{@Overridepublic void testb1() {}@Overridepublic void testb2() {}@Overridepublic void testc1() {}@Overridepublic void testc2() {}
}
最后,定义一个测试类Test
public class Test {public static void main(String[] args) {// 目标:认识接口。System.out.println(A.SCHOOL_NAME);// A a = new A();D d = new D();}
}
接口的好处
同学们,刚刚上面我们学习了什么是接口,以及接口的基本特点。那使用接口到底有什么好处呢?主要有下面的两点
- 弥补了类单继承的不足,一个类同时可以实现多个接口。
- 让程序可以面向接口编程,这样程序员可以灵活方便的切换各种业务实现。
我们看一个案例演示,假设有一个Studnet学生类,还有一个Driver司机的接口,还有一个Singer歌手的接口。
现在要写一个A类,想让他既是学生,偶然也是司机能够开车,偶尔也是歌手能够唱歌。那我们代码就可以这样设计,如下:
class Student{}interface Driver{void drive();
}interface Singer{void sing();
}//A类是Student的子类,同时也实现了Dirver接口和Singer接口
class A extends Student implements Driver, Singer{@Overridepublic void drive() {}@Overridepublic void sing() {}
}public class Test {public static void main(String[] args) {//想唱歌的时候,A类对象就表现为Singer类型Singer s = new A();s.sing();//想开车的时候,A类对象就表现为Driver类型Driver d = new A();d.drive();}
}
综上所述:接口弥补了单继承的不足,同时可以轻松实现在多种业务场景之间的切换。
接口的案例
各位同学,关于接口的特点以及接口的好处我们都已经学习完了。接下来我们做一个案例,先来看一下案例需求.
首先我们写一个学生类,用来描述学生的相关信息
public class Student {private String name;private char sex;private double score;public Student() {}public Student(String name, char sex, double score) {this.name = name;this.sex = sex;this.score = score;}public String getName() {return name;}public void setName(String name) {this.name = name;}public char getSex() {return sex;}public void setSex(char sex) {this.sex = sex;}public double getScore() {return score;}public void setScore(double score) {this.score = score;}
}
接着,写一个StudentOperator接口,表示学生信息管理系统的两个功能。
public interface StudentOperator {void printAllInfo(ArrayList<Student> students);void printAverageScore(ArrayList<Student> students);
}
然后,写一个StudentOperator接口的实现类StudentOperatorImpl1,采用第1套方案对业务进行实现。
public class StudentOperatorImpl1 implements StudentOperator{@Overridepublic void printAllInfo(ArrayList<Student> students) {System.out.println("----------全班全部学生信息如下--------------");for (int i = 0; i < students.size(); i++) {Student s = students.get(i);System.out.println("姓名:" + s.getName() + ", 性别:" + s.getSex() + ", 成绩:" + s.getScore());}System.out.println("-----------------------------------------");}@Overridepublic void printAverageScore(ArrayList<Student> students) {double allScore = 0.0;for (int i = 0; i < students.size(); i++) {Student s = students.get(i);allScore += s.getScore();}System.out.println("平均分:" + (allScore) / students.size());}
}
接着,再写一个StudentOperator接口的实现类StudentOperatorImpl2,采用第2套方案对业务进行实现。
public class StudentOperatorImpl2 implements StudentOperator{@Overridepublic void printAllInfo(ArrayList<Student> students) {System.out.println("----------全班全部学生信息如下--------------");int count1 = 0;int count2 = 0;for (int i = 0; i < students.size(); i++) {Student s = students.get(i);System.out.println("姓名:" + s.getName() + ", 性别:" + s.getSex() + ", 成绩:" + s.getScore());if(s.getSex() == '男'){count1++;}else {count2 ++;}}System.out.println("男生人数是:" + count1 + ", 女士人数是:" + count2);System.out.println("班级总人数是:" + students.size());System.out.println("-----------------------------------------");}@Overridepublic void printAverageScore(ArrayList<Student> students) {double allScore = 0.0;double max = students.get(0).getScore();double min = students.get(0).getScore();for (int i = 0; i < students.size(); i++) {Student s = students.get(i);if(s.getScore() > max) max = s.getScore();if(s.getScore() < min) min = s.getScore();allScore += s.getScore();}System.out.println("学生的最高分是:" + max);System.out.println("学生的最低分是:" + min);System.out.println("平均分:" + (allScore - max - min) / (students.size() - 2));}
}
再写一个班级管理类ClassManager,在班级管理类中使用StudentOperator的实现类StudentOperatorImpl1对学生进行操作
public class ClassManager {private ArrayList<Student> students = new ArrayList<>();private StudentOperator studentOperator = new StudentOperatorImpl1();public ClassManager(){students.add(new Student("迪丽热巴", '女', 99));students.add(new Student("古力娜扎", '女', 100));students.add(new Student("马尔扎哈", '男', 80));students.add(new Student("卡尔扎巴", '男', 60));}// 打印全班全部学生的信息public void printInfo(){studentOperator.printAllInfo(students);}// 打印全班全部学生的平均分public void printScore(){studentOperator.printAverageScore(students);}
}
最后,再写一个测试类Test,在测试类中使用ClassMananger完成班级学生信息的管理。
public class Test {public static void main(String[] args) {// 目标:完成班级学生信息管理的案例。ClassManager clazz = new ClassManager();clazz.printInfo();clazz.printScore();}
}
注意:如果想切换班级管理系统的业务功能,随时可以将StudentOperatorImpl1切换为StudentOperatorImpl2。
接口JDK8的新特性
各位同学,对于接口最常见的特性我们都学习完了。随着JDK版本的升级,在JDK8版本以后接口中能够定义的成员也做了一些更新,从JDK8开始,接口中新增的三种方法形式。
我们看一下这三种方法分别有什么特点?
public interface A {/*** 1、默认方法:必须使用default修饰,默认会被public修饰* 实例方法:对象的方法,必须使用实现类的对象来访问。*/default void test1(){System.out.println("===默认方法==");test2();}/*** 2、私有方法:必须使用private修饰。(JDK 9开始才支持的)* 实例方法:对象的方法。*/private void test2(){System.out.println("===私有方法==");}/*** 3、静态方法:必须使用static修饰,默认会被public修饰*/static void test3(){System.out.println("==静态方法==");}void test4();void test5();default void test6(){}
}
接下来我们写一个B类,实现A接口。B类作为A接口的实现类,只需要重写抽象方法就可以了,对于默认方法不需要子类重写。代码如下:
public class B implements A{@Overridepublic void test4() {}@Overridepublic void test5() {}
}
最后,写一个测试类,观察接口中的三种方法,是如何调用的
public class Test {public static void main(String[] args) {// 目标:掌握接口新增的三种方法形式B b = new B();b.test1(); //默认方法使用对象调用// b.test2(); //A接口中的私有方法,B类调用不了A.test3(); //静态方法,使用接口名调用}
}
综上所述:JDK8对接口新增的特性,有利于对程序进行扩展。
接口的其他细节
最后,给同学们介绍一下使用接口的其他细节,或者说注意事项:
- 一个接口可以继承多个接口
public class Test {public static void main(String[] args) {// 目标:理解接口的多继承。}
}interface A{void test1();
}
interface B{void test2();
}
interface C{}//比如:D接口继承C、B、A
interface D extends C, B, A{}//E类在实现D接口时,必须重写D接口、以及其父类中的所有抽象方法。
class E implements D{@Overridepublic void test1() {}@Overridepublic void test2() {}
}
接口除了上面的多继承特点之外,在多实现、继承和实现并存时,有可能出现方法名冲突的问题,需要了解怎么解决(仅仅只是了解一下,实际上工作中几乎不会出现这种情况)
1.一个接口继承多个接口,如果多个接口中存在相同的方法声明,则此时不支持多继承
2.一个类实现多个接口,如果多个接口中存在相同的方法声明,则此时不支持多实现
3.一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会有限使用父类的方法
4.一个类实现类多个接口,多个接口中有同名的默认方法,则这个类必须重写该方法。
综上所述:一个接口可以继承多个接口,接口同时也可以被类实现。