Java 面向对象高级
前面我们说过面向对象最核心的套路是: 设计对象来处理数据,解决问题。 如果你把面向对象高级这一部分课程学好,你设计出来的对象将更加好用。
在正式学习面向对象高级课程之前,我给大家提一些学习上的建议。目前我们学习的面向对象高级部分的知识点,就像一个一个的螺丝钉,在学习过程中你可能并不知道这些螺丝钉是用在哪里的,解决的什么问题。必须等这些内容都学习完之后,才能知道用这些螺丝钉可以用来搞飞机、造航母、造火箭。
所以,现阶段我们在学习过程中,主要关注下面几点就可以了。等整个基础课程学习完之后,再慢慢感在到哪里用。
1.静态
接下来,我们学习一下面向对象编程中很常见的一个关键字static.
static读作静态,可以用来修饰成员变量,也能修饰成员方法。我们先来学习static修饰成员变量。
1.1 static修饰成员变量
Java中的成员变量按照有无static修饰分为两种:类变量、实例变量。
由于静态变量是属于类的,只需要通过类名就可以调用:类名.静态变量
实例变量是属于对象的,需要通过对象才能调用:对象.实例变量
- 最后总结一下
- 1.类变量:属于类,在内存中只有一份,用类名调用
- 2.实例变量:属于对象,每一个对象都有一份,用对象调用
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
1.3 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修饰成员变量、和静态修饰成员方法这两种用法,到这里就学习完了。
1.4 工具类
学习完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){
...
}
}
1.5 static的注意事项
各位同学,到现在在我们已经学会了static修饰的变量、方法如何调用了。但是有一些注意事项还是需要给大家说明一下,目的是让大家知道,使用static写代码时,如果出错了,要知道为什么错、如何改正。
public class Student {
static String schoolName; // 类变量
double score; // 实例变量
// 1、类方法中可以直接访问类的成员,不可以直接访问实例成员。
public static void printHelloWorld(){
// 注意:同一个类中,访问类成员,可以省略类名不写。
schoolName = "黑马";
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 = "黑马2"; //对的
printHelloWorld2(); //对的
System.out.println(score); //对的
printPass2(); //对的
System.out.println(this); //对的
}
}
1.6 static应用(代码块)
各位同学,接下来我们再补充讲解一个知识点,叫代码块;代码块根据有无static修饰分为两种:静态代码块、实例代码块
我们先类学习静态代码块:
public class Student {
static int number = 80;
static String schoolName = "黑马";
// 静态代码块
static {
System.out.println("静态代码块执行了~~");
schoolName = "黑马";
}
}
静态代码块不需要创建对象就能够执行
public class Test {
public static void main(String[] args) {
// 目标:认识两种代码块,了解他们的特点和基本作用。
System.out.println(Student.number);
System.out.println(Student.number);
System.out.println(Student.number);
System.out.println(Student.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);
}
}
对于实例代码块重点注意:实例代码块每次创建对象之前都会执行一次
1.7 static应用(单例设计模式)
各位同学,关于static的应用我们再补充一个使用的技巧,叫做单例设计模式。
所谓设计模式指的是,一类问题可能会有多种解决方案,而设计模式是在编程实践中,多种方案中的一种最优方案。
关于静态的使用到这里,我们就学习完了。
2.继承
2.1 继承快速入门
各位同学,我们继续学习面向对象相关内容。面向对象编程之所以能够能够被广大开发者认可,有一个非常重要的原因,是因为它有三大特征,继承、封装和多态。接下来我们学习一下继承。
接下来,我们演示一下使用继承来编写代码,注意观察继承的特点。
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(); //错误
}
}
到这里,关于继承的基本使用我们就算学会了。为了让大家对继承有更深入的认识,我们来看看继承的内存原理。
这里我们只需要关注一点:子类对象实际上是由子、父类两张设计图共同创建出来的。
所以,在子类对象的空间中,既有本类的成员,也有父类的成员。但是子类只能调用父类公有的成员。
2.2 继承的好处
各位同学,学习完继承的快速入门之后,接下来我们学习一下继承的好处,以及它的应用场景。
我们通过一个案例来学习
观察代码发现,我们会发现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();
}
}
关于继承的好处我们只需要记住:继承可以提高代码的复用性
2.3 权限修饰符
各位同学,在刚才使用继承编写的代码中我们有用到两个权限修饰符,一个是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();
}
}
2.4 单继承、Object
刚才我们写的代码中,都是一个子类继承一个父类,那么有同学问到,一个子类可以继承多个父类吗?
Java语言只支持单继承,不支持多继承,但是可以多层继承。就像家族里儿子、爸爸和爷爷的关系一样:一个儿子只能有一个爸爸,不能有多个爸爸,但是爸爸也是有爸爸的。
public class Test {
public static void main(String[] args) {
// 目标:掌握继承的两个注意事项事项。
// 1、Java是单继承的:一个类只能继承一个直接父类;
// 2、Object类是Java中所有类的祖宗。
A a = new A();
B b = new B();
ArrayList list = new ArrayList();
list.add("java");
System.out.println(list.toString());
}
}
class A {} //extends Object{}
class B extends A{}
// class C extends B , A{} // 报错
class D extends B{}
2.5 方法重写
各位同学,学习完继承之后,在继承的基础之上还有一个很重要的现象需要给大家说一下。
叫做方法重写。为了让大家能够掌握方法重写,我们先认识什么是方法重写,再说一下方法的应用场景。
什么是方法重写
当子类觉得父类方法不好用,或者无法满足父类需求时,子类可以重写一个方法名称、参数列表一样的方法,去覆盖父类的这个方法,这就是方法重写。
注意:重写后,方法的访问遵循就近原则。下面我们看一个代码演示
写一个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");
}
// 方法重写
@Override
public 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("播妞", 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;
}
@Override
public String toString() {
return "Student{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
好了,到这里方法什么是方法重写,以及方法重写的应用场景我们就学习完了。
2.6 子类中访问成员的特点
各位同学,刚才我们已经学习了继承,我们发现继承至少涉及到两个类,而每一个类中都可能有各自的成员(成员变量、成员方法),就有可能出现子类和父类有相同成员的情况,那么在子类中访问其他成员有什么特点呢?
- 原则:在子类中访问其他成员(成员变量、成员方法),是依据就近原则的
定义一个父类,代码如下
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); // 局部名称
}
@Override
public 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); // 父类的成员变量
}
@Override
public void print1(){
System.out.println("==子类的print1方法执行了=");
}
public void showMethod(){
print1(); // 子类的
super.print1(); // 父类的
}
}
2.7 子类中访问构造器的特点
各位同学,我们知道一个类中可以写成员变量、成员方法,还有构造器。在继承关系下,子类访问成员变量和成员方法的特点我们已经学过了;接下来再学习子类中访问构造器的特点。
我们先认识子类构造器的语法特点,再讲一下子类构造器的应用场景
子类中访问构造器的语法规则
- 首先,子类全部构造器,都会先调用父类构造器,再执行自己。
子类访问构造器的应用场景
- 如果不想使用默认的
super()
方式调用父类构造器,还可以手动使用super(参数)
调用父类有参数构造器。
在本类中访问自己的构造方法
刚才我们学习了通过super()
和super(参数)
可以访问父类的构造器。有时候我们也需要访问自己类的构造器。语法如下
this(): 调用本类的空参数构造器
this(参数): 调用本类有参数的构造器
最后我们被this和super的用法在总结一下
访问本类成员:
this.成员变量 //访问本类成员变量
this.成员方法 //调用本类成员方法
this() //调用本类空参数构造器
this(参数) //调用本类有参数构造器
访问父类成员:
super.成员变量 //访问父类成员变量
super.成员方法 //调用父类成员方法
super() //调用父类空参数构造器
super(参数) //调用父类有参数构造器
注意:this和super访问构造方法,只能用到构造方法的第一句,否则会报错。
3.多态
接下来,我们学习面向对象三大特征的的最后一个特征——多态。
3.1 多态概述
什么是多态?
多态是在继承、实现情况下的一种现象,表现为:对象多态、行为多态。
比如:Teacher和Student都是People的子类
3=.2 多态的好处
各位同学,刚才我们认识了什么是多态。那么多态的写法有什么好处呢?
在多态形式下,右边的代码是解耦合的,更便于扩展和维护。
- 怎么理解这句话呢?比如刚开始p1指向Student对象,run方法执行的就是Student对象的业务;假如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("结束------------------------");
}
}
3.3 类型转换
虽然多态形式下有一些好处,但是也有一些弊端。在多态形式下,不能调用子类特有的方法,比如在Teacher类中多了一个teach方法,在Student类中多了一个study方法,这两个方法在多态形式下是不能直接调用的。
多态形式下不能直接调用子类特有方法,但是转型后是可以调用的。这里所说的转型就是把父类变量转换为子类类型。格式如下:
//如果p接收的是子类对象
if(父类变量 instance 子类){
//则可以将p转换为子类类型
子类 变量名 = (子类)父类变量;
}
如果类型转换错了,就会出现类型转换异常ClassCastException,比如把Teacher类型转换成了Student类型.
关于多态转型问题,我们最终记住一句话:原本是什么类型,才能还原成什么类型
4.final关键字
各位同学,接下来我们学习一个在面向对象编程中偶尔会用到的一个关键字叫final,也是为后面学习抽象类和接口做准备的。
4.1 final修饰符的特点
我们先来认识一下final的特点,final关键字是最终的意思,可以修饰类、修饰方法、修饰变量。
- final修饰类:该类称为最终类,特点是不能被继承
- final修饰方法:该方法称之为最终方法,特点是不能被重写。
- final修饰变量:该变量只能被赋值一次。
4.2 补充知识:常量
刚刚我们学习了final修饰符的特点,在实际运用当中经常使用final来定义常量。先说一下什么是Java中的常量?
- 被 static final 修饰的成员变量,称之为常量。
- 通常用于记录系统的配置信息
接下来我们用代码来演示一下
public class Constant {
//常量: 定义一个常量表示学校名称
//为了方便在其他类中被访问所以一般还会加上public修饰符
//常量命名规范:建议都采用大写字母命名,多个单词之前有_隔开
public static final String SCHOOL_NAME = "传智教育";
}
public class FinalDemo2 {
public static void main(String[] args) {
//由于常量是static的所以,在使用时直接用类名就可以调用
System.out.println(Constant.SCHOOL_NAME);
System.out.println(Constant.SCHOOL_NAME);
System.out.println(Constant.SCHOOL_NAME);
System.out.println(Constant.SCHOOL_NAME);
System.out.println(Constant.SCHOOL_NAME);
System.out.println(Constant.SCHOOL_NAME);
System.out.println(Constant.SCHOOL_NAME);
}
}
- 关于常量的原理,同学们也可以了解一下:在程序编译后,常量会“宏替换”,出现常量的地方,全都会被替换为其记住的字面量。把代码反编译后,其实代码是下面的样子
public class FinalDemo2 {
public static void main(String[] args) {
System.out.println("传智教育");
System.out.println("传智教育"E);
System.out.println("传智教育");
System.out.println("传智教育");
System.out.println("传智教育");
System.out.println("传智教育");
System.out.println("传智教育");
}
}
5.抽象
同学们,接下来我们学习Java中一种特殊的类,叫抽象类。为了让同学们掌握抽象类,会先让同学们认识一下什么是抽象类以及抽象类的特点,再学习一个抽象类的常见应用场景。
5.1 认识抽象类
我们先来认识一下什么是抽象类,以及抽象类有什么特点。
- 在Java中有一个关键字叫abstract,它就是抽象的意思,它可以修饰类也可以修饰方法。
- 被abstract修饰的类,就是抽象类
- 被abstract修饰的方法,就是抽象方法(不允许有方法体)
接下来用代码来演示一下抽象类和抽象方法
//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 {
@Override
public void test() {
}
}
- 子类继承父类如果不复写父类的抽象方法,要想不出错,这个子类也必须是抽象类
//B类基础A类,此时B类也是抽象类,这个时候就可以不重写A类的抽象方法
public abstract class B extends A {
}
5.2 抽象类的好处
接下来我们用一个案例来说一下抽象类的应用场景和好处。
分析需求发现,该案例中猫和狗都有名字这个属性,也都有叫这个行为,所以我们可以将共性的内容抽取成一个父类,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{
@Override
public 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.反过来用,我们不知道系统未来具体的业务实现时,我们可以先定义抽象类,将来让子类去实现,以方便系统的扩展。
5.3 模板方法模式
学习完抽象类的语法之后,接下来,我们学习一种利用抽象类实现的一种设计模式。先解释下一什么是设计模式?设计模式是解决某一类问题的最优方案。
设计模式在一些源码中经常会出现,还有以后面试的时候偶尔也会被问到,所以在合适的机会,就会给同学们介绍一下设计模式的知识。
那模板方法设计模式解决什么问题呢?模板方法模式主要解决方法中存在重复代码的问题
比如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{
@Override
public void doSing() {
System.out.println("我是一只小小小小鸟,想要飞就能飞的高~~~");
}
}
接着,再写一个B类继承C类,也复写doSing()方法,代码如下
public class B extends C{
@Override
public void doSing() {
System.out.println("我们一起学猫叫,喵喵喵喵喵喵喵~~");
}
}
最后,再写一个测试类Test
public class Test {
public static void main(String[] args) {
// 目标:搞清楚模板方法设计模式能解决什么问题,以及怎么写。
B b = new B();
b.sing();
}
}
综上所述:模板方法模式解决了多个子类中有相同代码的问题。具体实现步骤如下
第1步:定义一个抽象类,把子类中相同的代码写成一个模板方法。
第2步:把模板方法中不能确定的代码写成抽象方法,并在模板方法中调用。
第3步:子类继承抽象类,只需要父类抽象方法就可以了。
6.接口
同学们,接下来我们学习一个比抽象类抽象得更加彻底的一种特殊结构,叫做接口。在学习接口是什么之前,有一些事情需要给大家交代一下:Java已经发展了20多年了,在发展的过程中不同JDK版本的接口也有一些变化,所以我们在学习接口时,先以老版本为基础,学习完老版本接口的特性之后,再顺带着了解一些新版本接口的特性就可以了。
6.1 认识接口
我们先来认识一下接口?Java提供了一个关键字interface,用这个关键字来定义接口这种特殊结构。格式如下
public interface 接口名{
//成员变量(常量)
//成员方法(抽象方法)
}
按照接口的格式,我们定义一个接口看看
public interface A{
//这里public static final可以加,可以不加。
public static final String SCHOOL_NAME = "黑马程序员";
//这里的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{
@Override
public void testb1() {
}
@Override
public void testb2() {
}
@Override
public void testc1() {
}
@Override
public 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();
}
}
6.2 接口的好处
同学们,刚刚上面我们学习了什么是接口,以及接口的基本特点。那使用接口到底有什么好处呢?主要有下面的两点
- 弥补了类单继承的不足,一个类同时可以实现多个接口。
- 让程序可以面向接口编程,这样程序员可以灵活方便的切换各种业务实现。
我们看一个案例演示,假设有一个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{
@Override
public void drive() {
}
@Override
public 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();
}
}
综上所述:接口弥补了单继承的不足,同时可以轻松实现在多种业务场景之间的切换。
6.3 接口的案例
各位同学,关于接口的特点以及接口的好处我们都已经学习完了。接下来我们做一个案例,先来看一下案例需求.
首先我们写一个学生类,用来描述学生的相关信息
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{
@Override
public 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("-----------------------------------------");
}
@Override
public 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{
@Override
public 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("-----------------------------------------");
}
@Override
public 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。自己试试
6.4 接口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{
@Override
public void test4() {
}
@Override
public void test5() {
}
}
最后,写一个测试类,观察接口中的三种方法,是如何调用的
public class Test {
public static void main(String[] args) {
// 目标:掌握接口新增的三种方法形式
B b = new B();
b.test1(); //默认方法使用对象调用
// b.test2(); //A接口中的私有方法,B类调用不了
A.test3(); //静态方法,使用接口名调用
}
}
综上所述:JDK8对接口新增的特性,有利于对程序进行扩展。
6.5 接口的其他细节
最后,给同学们介绍一下使用接口的其他细节,或者说注意事项:
- 一个接口可以继承多个接口
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{
@Override
public void test1() {
}
@Override
public void test2() {
}
}
接口除了上面的多继承特点之外,在多实现、继承和实现并存时,有可能出现方法名冲突的问题,需要了解怎么解决(仅仅只是了解一下,实际上工作中几乎不会出现这种情况)
1.一个接口继承多个接口,如果多个接口中存在相同的方法声明,则此时不支持多继承
2.一个类实现多个接口,如果多个接口中存在相同的方法声明,则此时不支持多实现
3.一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会有限使用父类的方法
4.一个类实现类多个接口,多个接口中有同名的默认方法,则这个类必须重写该方法。
综上所述:一个接口可以继承多个接口,接口同时也可以被类实现。
7.内部类
内部类是类中的五大成分之一(成员变量、方法、构造器、内部类、代码块),如果一个类定义在另一个类的内部,这个类就是内部类。
当一个类的内部,包含一个完整的事物,且这个事物没有必要单独设计时,就可以把这个事物设计成内部类。
比如:汽车、的内部有发动机,发动机是包含在汽车内部的一个完整事物,可以把发动机设计成内部类。
public class Car{
//内部类
public class Engine{
}
}
内部类有四种形式,分别是成员内部类、静态内部类、局部内部类、匿名内部类。
我们先来学习成员内部类
7.1 成员内部类
成员内部类就是类中的一个普通成员,类似于成员变量、成员方法。
public class Outer {
private int age = 99;
public static String a="黑马";
// 成员内部类
public class Inner{
private String name;
private int age = 88;
//在内部类中既可以访问自己类的成员,也可以访问外部类的成员
public void test(){
System.out.println(age); //88
System.out.println(a); //黑马
int age = 77;
System.out.println(age); //77
System.out.println(this.age); //88
System.out.println(Outer.this.age); //99
}
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;
}
}
}
成员内部类如何创建对象,格式如下
//外部类.内部类 变量名 = new 外部类().new 内部类();
Outer.Inner in = new Outer().new Inner();
//调用内部类的方法
in.test();
总结一下内部类访问成员的特点
- 既可以访问内部类成员、也可以访问外部类成员
- 如果内部类成员和外部类成员同名,可以使用**
类名.this.成员
**区分
7.2 静态内部类
静态内部类,其实就是在成员内部类的前面加了一个static关键字。静态内部类属于外部类自己持有。
public class Outer {
private int age = 99;
public static String schoolName="黑马";
// 静态内部类
public static class Inner{
//静态内部类访问外部类的静态变量,是可以的;
//静态内部类访问外部类的实例变量,是不行的
public void test(){
System.out.println(schoolName); //99
//System.out.println(age); //报错
}
}
}
静态内部类创建对象时,需要使用外部类的类名调用。
//格式:外部类.内部类 变量名 = new 外部类.内部类();
Outer.Inner in = new Outer.Inner();
in.test();
7.3 局部内部类
局部内部类是定义在方法中的类,和局部变量一样,只能在方法中有效。所以局部内部类的局限性很强,一般在开发中是不会使用的。
public class Outer{
public void test(){
//局部内部类
class Inner{
public void show(){
System.out.println("Inner...show");
}
}
//局部内部类只能在方法中创建对象,并使用
Inner in = new Inner();
in.show();
}
}
7.4 匿名内部类
接下来学习一种再实际开发中用得最多的一种内部类,叫匿名内部类。相比于前面几种内部类,匿名内部类就比较重要的。
我们还是先认识一下什么是匿名内部类?
匿名内部类是一种特殊的局部内部类;所谓匿名,指的是程序员不需要为这个类声明名字。
下面就是匿名内部类的格式:
new 父类/接口(参数值){
@Override
重写父类/接口的方法;
}
匿名内部类本质上是一个没有名字的子类对象、或者接口的实现类对象。
比如,先定义一个Animal抽象类,里面定义一个cry()方法,表示所有的动物有叫的行为,但是因为动物还不具体,cry()这个行为并不能具体化,所以写成抽象方法。
public abstract class Animal{
public abstract void cry();
}
接下来,我想要在不定义子类的情况下创建Animal的子类对象,就可以使用匿名内部类
public class Test{
public static void main(String[] args){
//这里后面new 的部分,其实就是一个Animal的子类对象
//这里隐含的有多态的特性: Animal a = Animal子类对象;
Animal a = new Animal(){
@Override
public void cry(){
System.out.println("猫喵喵喵的叫~~~");
}
}
a.eat(); //直线上面重写的cry()方法
}
}
需要注意的是,匿名内部类在编写代码时没有名字,编译后系统会为自动为匿名内部类生产字节码,字节码的名称会以外部类$1.class
的方法命名
匿名内部类的作用:简化了创建子类对象、实现类对象的书写格式。
学习完匿名内部类的基本使用之后,我们再来看一下匿名内部类在实际中的应用场景。其实一般我们会主动的使用匿名内部类。
**只有在调用方法时,当方法的形参是一个接口或者抽象类,为了简化代码书写,而直接传递匿名内部类对象给方法。**这样就可以少写一个类。比如,看下面代码
public interface Swimming{
public void swim();
}
public class Test{
public static void main(String[] args){
Swimming s1 = new Swimming(){
public void swim(){
System.out.println("狗刨飞快");
}
};
go(s1);
Swimming s1 = new Swimming(){
public void swim(){
System.out.println("猴子游泳也还行");
}
};
go(s1);
}
//形参是Swimming接口,实参可以接收任意Swimming接口的实现类对象
public static void go(Swimming s){
System.out.println("开始~~~~~~~~");
s.swim();
System.out.println("结束~~~~~~~~");
}
}
8.枚举
8.1 认识枚举
同学们,接下来我们学习一个新的知识点,枚举。枚举是我们以后在项目开发中偶尔会用到的知识。话不多说,我们还是先来认识一下枚举。
枚举是一种特殊的类,它的格式是:
public enum 枚举类名{
枚举项1,枚举项2,枚举项3;
}
其实枚举项就表示枚举类的对象,只是这些对象在定义枚举类时就预先写好了,以后就只能用这几个固定的对象。
我们用代码演示一下,定义一个枚举类A,在枚举类中定义三个枚举项X, Y, Z
public enum A{
X,Y,Z;
}
想要获取枚举类中的枚举项,只需要用类名调用就可以了
public class Test{
public static void main(String[] args){
//获取枚举A类的,枚举项
A a1 = A.X;
A a2 = A.Y;
A a3 = A.Z;
}
}
刚才说,枚举项实际上是枚举类的对象,这一点其实可以通过反编译的形式来验证(需要用到反编译的命令,这里不能直接将字节码拖进idea反编译)
我们会看到,枚举类A是用class定义的,说明枚举确实是一个类,而且X,Y,Z都是A类的对象;而且每一个枚举项都是被public static final
修饰,所以被可以类名调用,而且不能更改。
既然枚举是一个类的话,我们能不能在枚举类中定义构造器、成员变量、成员方法呢?答案是可以的。来看一下代码吧
public enum A{
//定义枚举项
X,Y,Z("张三"); //枚举项后面加括号,就是在执行枚举类的带参数构造方法。
//定义空构造器
public A(){
}
//成员变量
private String name;
//定义带参数构造器
public A(String name){
this.name=name;
}
//成员方法
public String getName(){
return name;
}
...
}
虽然枚举类中可以像类一样,写一些类的其他成员,但是一般不会这么写,如果你真要这么干的话,到不如直接写普通类来的直接。
8.2 枚举的应用场景
刚才我们认识了一下什么是枚举,接下来我们看一下枚举在实际中的运用,枚举的应用场景是这样的:枚举一般表示一组信息,然后作为参数进行传输。
我们来看一个案例。比如我们现在有这么一个应用,用户进入应用时,需要让用户选择是女生、还是男生,然后系统会根据用户选择的是男生,还是女生推荐不同的信息给用户观看。
这里我们就可以先定义一个枚举类,用来表示男生、或者女生
public class Constant{
BOY,GRIL
}
再定义一个测试类,完成用户进入系统后的选择
public class Test{
public static void main(String[] args){
//调用方法,传递男生
provideInfo(Constant.BOY);
}
public static void provideInfo(Constant c){
switch(c){
case BOY:
System.out.println("展示一些信息给男生看");
break;
case GRIL:
System.out.println("展示一些信息给女生看");
break;
}
}
}
最终再总结一下枚举的应用场景:枚举一般表示几个固定的值,然后作为参数进行传输。