day01——面向对象高级
各位同学,接下来的三天课程中,我们继续学习面向对象的相关课程。面向对象是写Java程序的核心套路,如何你不懂面向对象,那就相当于Java你白学了。所以在接下来的三天时间里,各位同学也需要克服重重困难好好学习。
前面我们说过面向对象最核心的套路是:设计对象来处理数据,解决问题。 如果你把面向对象高级这一部分课程学好,你设计出来的对象将更加好用。
在正式学习面向对象高级课程之前,我给大家提一些学习上的建议。目前我们学习的面向对象高级部分的知识点,就像一个一个的螺丝钉,在学习过程中你可能并不知道这些螺丝钉是用在哪里的,解决的什么问题。必须等这些内容都学习完之后,才能知道用这些螺丝钉可以用来搞飞机、造航母、造火箭。

所以,现阶段我们在学习过程中,主要关注下面几点就可以了。等整个基础课程学习完之后,再慢慢感在到哪里用。

一、静态
接下来,我们学习一下面向对象编程中很常见的一个关键字static.
static读作静态,可以用来修饰成员变量,也能修饰成员方法。我们先来学习static修饰成员变量。
1.1 static修饰成员变量
Java中的成员变量按照有无static修饰分为两种:类变量、实例变量。它们的区别如下图所示:

由于静态变量是属于类的,只需要通过类名就可以调用:类名.静态变量
,只要在一个对象修改,类的全部对象都修改
实例变量是属于对象的,需要通过对象才能调用:对象.实例变量
- 下面是代码演示(注意静态变量,和实例变量是如何调用的)

为了让大家对于这两种成员变量的执行过程理解更清楚一点,在这里给大家在啰嗦几句,我们来看一下上面代码的内存原理。

1 2
| - 1.类变量:属于类,在内存中只有一份,用类名调用 - 2.实例变量:属于对象,每一个对象都有一份,用对象调用
|
1.2 static修饰成员变量的应用场景
学习完static修饰成员变量的基本使用之后,接下来我们学习一下static修饰成员变量在实际工作中的应用。
在实际开发中,如果某个数据只需要一份,且希望能够被共享(访问、修改),则该数据可以定义成类变量来记住。
我们看一个案例**
需求:系统启动后,要求用于类可以记住自己创建了多少个用户对象。**
- 第一步:先定义一个
User
类,在用户类中定义一个static修饰的变量,用来表示在线人数;
1 2 3 4 5 6 7
| public class User{ public static int number; public User(){ User.number++; } }
|
- 第二步:再写一个测试类,再测试类中创建4个User对象,再打印number的值,观察number的值是否再自增。
1 2 3 4 5 6 7 8 9 10 11 12
| public class Test{ public static void main(String[] args){ new User(); new User(); new User(); new User(); System.out.println("系统创建的User对象个数:"+User.number); } }
|
运行上面的代码,查看执行结果是:系统创建的User对象个数:4
1.3 static修饰成员方法
各位同学,学习完static修饰成员变量之后,接下来我们学习static修饰成员方法。成员方法根据有无static也分为两类:类方法、实例方法

有static修饰的方法,是属于类的,称为类方法;调用时直接用类名调用即可。类方法意思是可以直接用类调用的方法
无static修饰的方法,是属于对象的,称为实例方法;调用时,需要使用对象调用。
我们看一个案例,演示类方法、实例方法的基本使用
- 先定义一个Student类,在类中定义一个类方法、定义一个实例方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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?"成绩合格":"成绩不合格"); } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Test2{ public static void main(String[] args){ Student.printHelloWorld(); Student s = new Student(); s.printPass(); s.printHelloWorld(); } }
|
搞清楚类方法和实例方法如何调用之后,接下来再啰嗦几句,和同学们聊一聊static修饰成员方法的内存原理。
1 2
| 1.类方法:static修饰的方法,可以被类名调用,是因为它是随着类的加载而加载的;所以类名直接就可以找到static修饰的方法 2.实例方法:非static修饰的方法,需要创建对象后才能调用,是因为实例方法中可能会访问实例变量,而实例变量需要创建对象后才存在。所以实例方法,必须创建对象后才能调用。
|

关于static修饰成员变量、和静态修饰成员方法这两种用法,到这里就学习完了。
1.4 工具类
学习完static修饰方法之后,我们讲一个有关类方法的应用知识,叫做工具类。
如果一个类中的方法全都是静态的,那么这个类中的方法就全都可以被类名直接调用,由于调用起来非常方便,就像一个工具一下,所以把这样的类就叫做工具类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class MyUtils{ public static String createCode(int n){ String code = ""; String data = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKMNOPQRSTUVWXYZ"; Random r = new Random(); for(int i=0; i<n; i++){ int index = r.nextInt(data.length()); char ch = data.charAt(index); code+=ch; } return code; } }
|
- 接着可以在任何位置调用
MyUtils
的createCOde()方法
产生任意个数的验证码
1 2 3 4 5 6
| public class LoginDemo{ public static void main(String[] args){ System.out.println(MyUtils.createCode()); } }
|
1 2 3 4 5 6
| public class registerDemo{ public static void main(String[] args){ System.out.println(MyUtils.createCode()); } }
|
工具类的使用就是这样子的,学会了吗?
在补充一点,工具类里的方法全都是静态的,推荐用类名调用为了防止使用者用对象调用。我们可以把工具类的构造方法私有化。
1 2 3 4 5 6 7 8 9 10 11
| public class MyUtils{ private MyUtils(){ } public static String createCode(int n){ ... } }
|
1.5 static的注意事项
各位同学,到现在在我们已经学会了static修饰的变量、方法如何调用了。但是有一些注意事项还是需要给大家说明一下,目的是让大家知道,使用static写代码时,如果出错了,要知道为什么错、如何改正。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| public class Student { static String schoolName; double score;
public static void printHelloWorld(){ schoolName = "黑马"; printHelloWorld2();
System.out.println(score); printPass();
ystem.out.println(this); } public static void printHelloWorld2(){
} public void printPass2(){
} public void printPass(){ schoolName = "黑马2"; printHelloWorld2();
System.out.println(score); printPass2();
System.out.println(this); } }
|
1.6 static应用(代码块)
各位同学,接下来我们再补充讲解一个知识点,叫代码块;代码块根据有无static修饰分为两种:静态代码块、实例代码块
我们先类学习静态代码块:

1 2 3 4 5 6 7 8 9 10
| public class Student { static int number = 80; static String schoolName = "黑马"; static { System.out.println("静态代码块执行了~~"); schoolName = "黑马"; } }
|
静态代码块不需要创建对象就能够执行
1 2 3 4 5 6 7 8 9 10
| 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); } }
|
执行上面代码时,发现没有创建对象,静态代码块就已经执行了。

关于静态代码块重点注意:静态代码块,随着类的加载而执行,而且只执行一次。
再来学习一下实例代码块

实例代码块的作用和构造器的作用是一样的,用来给对象初始化值;而且每次创建对象之前都会先执行实例代码块。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| 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("有参数构造器执行了~~"); } }
|
接下来在测试类中进行测试,观察创建对象时,实例代码块是否先执行了。
1 2 3 4 5 6 7 8
| 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.1 继承快速入门
各位同学,我们继续学习面向对象相关内容。面向对象编程之所以能够能够被广大开发者认可,有一个非常重要的原因,是因为它有三大特征,继承、封装和多态。封装我们在基础班已经学过了,接下来我们学习一下继承。

接下来,我们演示一下使用继承来编写代码,注意观察继承的特点。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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
1 2 3 4 5 6 7 8 9 10 11
| public class B extends A{ public void print3(){ System.out.println(i); print1(); System.out.println(j); print2(); } }
|
接下来,我们再演示一下,创建B类对象,能否调用父类A的成员。再写一个测试类
1 2 3 4 5 6 7 8 9 10 11 12
| 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公有的成员。
1 2 3 4 5 6 7 8 9 10
| public class People{ private String name; public String getName(){ return name; } public void setName(String name){ this.name=name; } }
|
- 再写两个子类Teacher继承People类,同时在子类中加上自己特有的成员。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| 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对象,并调用方法。
1 2 3 4 5 6 7 8 9 10 11
| 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(受保护的)、一个是缺省的(不写任何修饰符)。
接下来我们就学习一下这四个权限修饰符分别有什么作用。
什么是权限修饰符呢?
权限修饰符是用来限制类的成员(成员变量、成员方法、构造器…)能够被访问的范围。
每一种权限修饰符能够被访问的范围如下

下面我们用代码演示一下,在本类中可以访问到哪些权限修饰的方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| public class Fu { private void privateMethod(){ System.out.println("==private=="); }
void method(){ System.out.println("==缺省=="); }
protected void protectedMethod(){ System.out.println("==protected=="); }
public void publicMethod(){ System.out.println("==public=="); }
public void test(){ privateMethod(); method(); protectedMethod(); publicMethod(); } }
|
接下来,在和Fu类同一个包下,创建一个测试类Demo,演示同一个包下可以访问到哪些权限修饰的方法。
1 2 3 4 5 6 7 8 9
| public class Demo { public static void main(String[] args) { Fu f = new Fu(); f.method(); f.protectedMethod(); f.publicMethod(); } }
|
接下来,在另一个包下创建一个Fu类的子类,演示不同包下的子类中可以访问哪些权限修饰的方法。
1 2 3 4 5 6 7 8 9
| public class Zi extends Fu { public void test(){ protectedMethod(); publicMethod(); } }
|
接下来,在和Fu类不同的包下,创建一个测试类Demo2,演示一下不同包的无关类,能访问到哪些权限修饰的方法;
1 2 3 4 5 6 7 8 9 10 11 12
| public class Demo2 { public static void main(String[] args) { Fu f = new Fu(); f.publicMethod();
Zi zi = new Zi(); } }
|
2.4 单继承、Object
刚才我们写的代码中,都是一个子类继承一个父类,那么有同学问到,一个子类可以继承多个父类吗?
Java语言只支持单继承,不支持多继承,但是可以多层继承。就像家族里儿子、爸爸和爷爷的关系一样:一个儿子只能有一个爸爸,不能有多个爸爸,但是爸爸也是有爸爸的,但是一个爸爸可以有多个儿子。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class Test { public static void main(String[] args) { A a = new A(); B b = new B();
ArrayList list = new ArrayList(); list.add("java"); System.out.println(list.toString()); } }
class A {} class B extends A{}
class D extends B{}
|
2.5 方法重写
各位同学,学习完继承之后,在继承的基础之上还有一个很重要的现象需要给大家说一下。
叫做方法重写。为了让大家能够掌握方法重写,我们先认识什么是方法重写,再说一下方法的应用场景。
什么是方法重写
当子类觉得父类方法不好用,或者无法满足父类需求时,子类可以重写一个方法名称、参数列表一样的方法,去覆盖父类的这个方法,这就是方法重写。
注意:重写后,方法的访问遵循就近原则。下面我们看一个代码演示
写一个A类作为父类,定义两个方法print1和print2
1 2 3 4 5 6 7 8 9
| 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方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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类对象,调用方法
1 2 3 4 5 6 7 8
| public class Test { public static void main(String[] args) { B b = new B(); b.print1(); b.print2(2, 3); } }
|
执行代码,我们发现真正执行的是B类中的print1和print2方法

知道什么是方法重写之后,还有一些注意事项,需要和大家分享一下。
1 2 3 4 5
| - 1.重写的方法上面,可以加一个注解@Override,用于标注这个方法是复写的父类方法 - 2.子类复写父类方法时,访问权限必须大于或者等于父类方法的权限 public > protected > 缺省 - 3. 重写的方法返回值类型,必须与被重写的方法返回值类型一样,或者范围更小 - 4. 私有方法、静态方法不能被重写,如果重写会报错。
|
关于这些注意事项,同学们其实只需要了解一下就可以了。实际上我们实际写代码时,只要和父类写的一样就可以( 总结起来就8个字:声明不变,重新实现)
方法重写的应用场景
学习完方法重写之后,接下来,我们还需要大家掌握方法重写,在实际中的应用场景。方法重写的应用场景之一就是:子类重写Object的toString()方法,以便返回对象的内容。
比如:有一个Student类,这个类会默认继承Object类。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| 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()方法,会得到对象的地址值。
1 2 3 4 5 6 7
| public class Test { public static void main(String[] args) { Student s = new Student("播妞", 19); System.out.println(s); } }
|

但是,此时不想调用父类Object的toString()方法,那就可以在Student类中重新写一个toSting()方法,用于返回对象的属性值。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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 子类中访问成员的特点
各位同学,刚才我们已经学习了继承,我们发现继承至少涉及到两个类,而每一个类中都可能有各自的成员(成员变量、成员方法),就有可能出现子类和父类有相同成员的情况,那么在子类中访问其他成员有什么特点呢?
- 原则:在子类中访问其他成员(成员变量、成员方法),是依据就近原则的
定义一个父类,代码如下
1 2 3 4 5 6 7
| public class F { String name = "父类名字";
public void print1(){ System.out.println("==父类的print1方法执行=="); } }
|
再定义一个子类,代码如下。有一个同名的name成员变量,有一个同名的print1成员方法;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| 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(); } }
|
接下来写一个测试类,观察运行结果,我们发现都是调用的子类变量、子类方法。
1 2 3 4 5 6 7 8
| public class Test { public static void main(String[] args) { Z z = new Z(); z.showName(); z.showMethod(); } }
|
- 如果子类和父类出现同名变量或者方法,优先使用子类的;此时如果一定要在子类中使用父类的成员,可以加this或者super进行区分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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(参数)
可以访问父类的构造器。有时候我们也需要访问自己类的构造器。语法如下
1 2
| this(): 调用本类的空参数构造器 this(参数): 调用本类有参数的构造器
|

最后我们被this和super的用法在总结一下
1 2 3 4 5 6 7 8 9 10 11 12 13
| 访问本类成员: this.成员变量 this.成员方法 this() this(参数) 访问父类成员: super.成员变量 super.成员方法 super() super(参数) 注意:this和super访问构造方法,只能用到构造方法的第一句,否则会报错。
|
day02——面向对象高级
今天我们继续学习面向对象的语法知识,我们今天学习的主要内容是:多态、抽象、接口。
学会这些语法知识,可以让我们编写代码更灵活,代码的复用性更高。
一、多态
接下来,我们学习面向对象三大特征的的最后一个特征——多态。
1.1 多态概述
什么是多态?
多态是在继承、实现情况下的一种现象,表现为:对象多态、行为多态。
同一个行为具有多个不同表现形式或形态的能力,现实中,比如我们按下 F1 键这个动作:
- 如果当前在 Flash 界面下弹出的就是 AS 3 的帮助文档;
- 如果当前在 Word 下弹出的就是 Word 帮助;
- 在 Windows 下弹出的就是 Windows 帮助和支持。
同一个事件发生在不同的对象上会产生不同的结果。
当使用多态方式调用方法时,首先检查父类中是否有该方法,如果没有,则编译错误;如果有,再去调用子类的同名方法。
比如:Teacher和Student都是People的子类,代码可以写成下面的样子


1.2 多态的好处
各位同学,刚才我们认识了什么是多态。那么多态的写法有什么好处呢?
在多态形式下,右边的代码是解耦合的,更便于扩展和维护。
- 怎么理解这句话呢?比如刚开始p1指向Student对象,run方法执行的就是Student对象的业务;假如p1指向Teacher对象 ,run方法执行的自然是Teacher对象的业务。

定义方法时,使用父类类型作为形参,可以接收一切子类对象,扩展行更强,更便利。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| public class Test2 { public static void main(String[] args) { Teacher t = new Teacher(); go(t);
Student s = new Student(); go(s); }
public static void go(People p){ System.out.println("开始------------------------"); p.run(); System.out.println("结束------------------------"); } }
|
1.3 类型转换
虽然多态形式下有一些好处,但是也有一些弊端。在多态形式下,不能调用子类特有的方法,比如在Teacher类中多了一个teach方法,在Student类中多了一个study方法,这两个方法在多态形式下是不能直接调用的。

多态形式下不能直接调用子类特有方法,但是转型后是可以调用的。这里所说的转型就是把父类变量转换为子类类型。格式如下:
1 2 3 4 5
| if(父类变量 instance 子类){ 子类 变量名 = (子类)父类变量; }
|

如果类型转换错了,就会出现类型转换异常ClassCastException,比如把Teacher类型转换成了Student类型.

关于多态转型问题,我们最终记住一句话:原本是什么类型,才能还原成什么类型
二、final关键字
各位同学,接下来我们学习一个在面向对象编程中偶尔会用到的一个关键字叫final,也是为后面学习抽象类和接口做准备的。
2.1 final修饰符的特点
我们先来认识一下final的特点,final关键字是最终的意思,可以修饰类、修饰方法、修饰变量。
1 2 3
| - final修饰类:该类称为最终类,特点是不能被继承 - final修饰方法:该方法称之为最终方法,特点是不能被重写。 - final修饰变量:该变量只能被赋值一次。
|
- 接下来我们分别演示一下,先看final修饰类的特点

再来演示一下final修饰方法的特点

再演示一下final修饰变量的特点




2.2 补充知识:常量
刚刚我们学习了final修饰符的特点,在实际运用当中经常使用final来定义常量。先说一下什么是Java中的常量?
- 被 static final 修饰的成员变量,称之为常量。
- 通常用于记录系统的配置信息
接下来我们用代码来演示一下
1 2 3 4 5 6
| public class Constant { public static final String SCHOOL_NAME = "传智教育"; }
|
1 2 3 4 5 6 7 8 9 10 11 12
| public class FinalDemo2 { public static void main(String[] args) { 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); } }
|
- 关于常量的原理,同学们也可以了解一下:在程序编译后,常量会“宏替换”,出现常量的地方,全都会被替换为其记住的字面量。把代码反编译后,其实代码是下面的样子
1 2 3 4 5 6 7 8 9 10 11
| 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("传智教育"); } }
|
三、抽象
同学们,接下来我们学习Java中一种特殊的类,叫抽象类。为了让同学们掌握抽象类,会先让同学们认识一下什么是抽象类以及抽象类的特点,再学习一个抽象类的常见应用场景。
3.1 认识抽象类
我们先来认识一下什么是抽象类,以及抽象类有什么特点。
- 在Java中有一个关键字叫abstract,它就是抽象的意思,它可以修饰类也可以修饰方法。
1 2
| - 被abstract修饰的类,就是抽象类 - 被abstract修饰的方法,就是抽象方法(不允许有方法体)
|
接下来用代码来演示一下抽象类和抽象方法
1 2 3 4 5
| public abstract class A{ public abstract void test(); }
|
- 类的成员(成员变量、成员方法、构造器),类的成员都可以有。如下面代码,抽象类也可以有实例方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| 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; } }
|

- 抽象类虽然不能创建对象,但是它可以作为父类让子类继承。而且子类继承父类必须重写父类的所有抽象方法。
1 2 3 4 5 6 7
| public class B extends A { @Override public void test() {
} }
|
- 子类继承父类如果不复写父类的抽象方法,要想不出错,这个子类也必须是抽象类
1 2 3 4
| public abstract class B extends A {
}
|
3.2 抽象类的好处
接下来我们用一个案例来说一下抽象类的应用场景和好处。需求如下图所示

分析需求发现,该案例中猫和狗都有名字这个属性,也都有叫这个行为,所以我们可以将共性的内容抽取成一个父类,Animal类,但是由于猫和狗叫的声音不一样,于是我们在Animal类中将叫的行为写成抽象的。代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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类。代码如下
1 2 3 4 5
| public class Dog extends Animal{ public void cry(){ System.out.println(getName() + "汪汪汪的叫~~"); } }
|
然后,再写一个Animal的子类,Cat类。代码如下
1 2 3 4 5
| public class Cat extends Animal{ public void cry(){ System.out.println(getName() + "喵喵喵的叫~~"); } }
|
最后,再写一个测试类,Test类。
1 2 3 4 5 6 7
| public class Test2 { public static void main(String[] args) { Animal a = new Dog(); a.cry(); } }
|
再学一招,假设现在系统有需要加一个Pig类,也有叫的行为,这时候也很容易原有功能扩展。只需要让Pig类继承Animal,复写cry方法就行。
1 2 3 4 5 6
| public class Pig extends Animal{ @Override public void cry() { System.out.println(getName() + "嚯嚯嚯~~~"); } }
|
此时,创建对象时,让Animal接收Pig,就可以执行Pig的cry方法
1 2 3 4 5 6 7
| public class Test2 { public static void main(String[] args) { Animal a = new Pig(); a.cry(); } }
|
综上所述,我们总结一下抽象类的使用场景和好处
1 2 3
| 1.用抽象类可以把父类中相同的代码,包括方法声明都抽取到父类,这样能更好的支持多态,一提高代码的灵活性。
2.反过来用,我们不知道系统未来具体的业务实现时,我们可以先定义抽象类,将来让子类去实现,以方便系统的扩展。
|
3.3 模板方法模式
学习完抽象类的语法之后,接下来,我们学习一种利用抽象类实现的一种设计模式。先解释下一什么是设计模式?设计模式是解决某一类问题的最优方案。
设计模式在一些源码中经常会出现,还有以后面试的时候偶尔也会被问到,所以在合适的机会,就会给同学们介绍一下设计模式的知识。
那模板方法设计模式解决什么问题呢?模板方法模式主要解决方法中存在重复代码的问题
比如A类和B类都有sing()方法,sing()方法的开头和结尾都是一样的,只是中间一段内容不一样。此时A类和B类的sing()方法中就存在一些相同的代码。

怎么解决上面的重复代码问题呢? 我们可以写一个抽象类C类,在C类中写一个doSing()的抽象方法。再写一个sing()方法,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13
| public abstract class C { public final void sing(){ System.out.println("唱一首你喜欢的歌:");
doSing();
System.out.println("唱完了!"); }
public abstract void doSing(); }
|
然后,写一个A类继承C类,复写doSing()方法,代码如下
1 2 3 4 5 6
| public class A extends C{ @Override public void doSing() { System.out.println("我是一只小小小小鸟,想要飞就能飞的高~~~"); } }
|
接着,再写一个B类继承C类,也复写doSing()方法,代码如下
1 2 3 4 5 6
| public class B extends C{ @Override public void doSing() { System.out.println("我们一起学猫叫,喵喵喵喵喵喵喵~~"); } }
|
最后,再写一个测试类Test
1 2 3 4 5 6 7
| public class Test { public static void main(String[] args) { B b = new B(); b.sing(); } }
|
综上所述:模板方法模式解决了多个子类中有相同代码的问题。具体实现步骤如下
1 2 3
| 第1步:定义一个抽象类,把子类中相同的代码写成一个模板方法。 第2步:把模板方法中不能确定的代码写成抽象方法,并在模板方法中调用。 第3步:子类继承抽象类,只需要父类抽象方法就可以了。
|
四、接口
同学们,接下来我们学习一个比抽象类抽象得更加彻底的一种特殊结构,叫做接口。在学习接口是什么之前,有一些事情需要给大家交代一下:Java已经发展了20多年了,在发展的过程中不同JDK版本的接口也有一些变化,所以我们在学习接口时,先以老版本为基础,学习完老版本接口的特性之后,再顺带着了解一些新版本接口的特性就可以了。
4.1 认识接口
我们先来认识一下接口?Java提供了一个关键字interface,用这个关键字来定义接口这种特殊结构。格式如下
1 2 3 4
| public interface 接口名{ }
|
按照接口的格式,我们定义一个接口看看
1 2 3 4 5 6 7
| public interface A{ public static final String SCHOOL_NAME = "黑马程序员"; public abstract void test(); }
|
写好A接口之后,在写一个测试类,用一下
1 2 3 4 5 6 7 8 9
| public class Test{ public static void main(String[] args){ System.out.println(A.SCHOOL_NAME); A a = new A(); } }
|
我们发现定义好接口之后,是不能创建对象的。那接口到底什么使用呢?需要我注意下面两点
- 接口是用来被类实现(implements)的,我们称之为实现类。
- 一个类是可以实现多个接口的(接口可以理解成干爹),类实现接口必须重写所有接口的全部抽象方法,否则这个类也必须是抽象类
比如,再定义一个B接口,里面有两个方法testb1(),testb2()
1 2 3 4
| public interface B { void testb1(); void testb2(); }
|
接着,再定义一个C接口,里面有两个方法testc1(), testc2()
1 2 3 4
| public interface C { void testc1(); void testc2(); }
|
然后,再写一个实现类D,同时实现B接口和C接口,此时就需要复写四个方法,如下代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public class D implements B, C{ @Override public void testb1() {
}
@Override public void testb2() {
}
@Override public void testc1() {
}
@Override public void testc2() {
} }
|
最后,定义一个测试类Test
1 2 3 4 5 6 7 8 9
| public class Test { public static void main(String[] args) { System.out.println(A.SCHOOL_NAME);
D d = new D(); } }
|
4.2 接口的好处
同学们,刚刚上面我们学习了什么是接口,以及接口的基本特点。那使用接口到底有什么好处呢?主要有下面的两点
- 弥补了类单继承的不足,一个类同时可以实现多个接口。
- 让程序可以面向接口编程,这样程序员可以灵活方便的切换各种业务实现。
我们看一个案例演示,假设有一个Studnet学生类,还有一个Driver司机的接口,还有一个Singer歌手的接口。
现在要写一个A类,想让他既是学生,偶然也是司机能够开车,偶尔也是歌手能够唱歌。那我们代码就可以这样设计,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| class Student{
}
interface Driver{ void drive(); }
interface Singer{ void sing(); }
class A extends Student implements Driver, Singer{ @Override public void drive() {
}
@Override public void sing() {
} }
public class Test { public static void main(String[] args) { Singer s = new A(); s.sing(); Driver d = new A(); d.drive(); } }
|
综上所述:接口弥补了单继承的不足,同时可以轻松实现在多种业务场景之间的切换。
4.3 接口的案例
各位同学,关于接口的特点以及接口的好处我们都已经学习完了。接下来我们做一个案例,先来看一下案例需求.

首先我们写一个学生类,用来描述学生的相关信息
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| 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接口,表示学生信息管理系统的两个功能。
1 2 3 4
| public interface StudentOperator { void printAllInfo(ArrayList<Student> students); void printAverageScore(ArrayList<Student> students); }
|
然后,写一个StudentOperator接口的实现类StudentOperatorImpl1,采用第1套方案对业务进行实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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套方案对业务进行实现。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| 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对学生进行操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| 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完成班级学生信息的管理。
1 2 3 4 5 6 7 8
| public class Test { public static void main(String[] args) { ClassManager clazz = new ClassManager(); clazz.printInfo(); clazz.printScore(); } }
|
注意:如果想切换班级管理系统的业务功能,随时可以将StudentOperatorImpl1切换为StudentOperatorImpl2。自己试试
4.4 接口JDK8的新特性
各位同学,对于接口最常见的特性我们都学习完了。随着JDK版本的升级,在JDK8版本以后接口中能够定义的成员也做了一些更新,从JDK8开始,接口中新增的三种方法形式。
我们看一下这三种方法分别有什么特点?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public interface A {
default void test1(){ System.out.println("===默认方法=="); test2(); }
private void test2(){ System.out.println("===私有方法=="); }
static void test3(){ System.out.println("==静态方法=="); }
void test4(); void test5(); default void test6(){
} }
|
接下来我们写一个B类,实现A接口。B类作为A接口的实现类,只需要重写抽象方法就尅了,对于默认方法不需要子类重写。代码如下:
1 2 3 4 5 6 7 8 9 10 11
| public class B implements A{ @Override public void test4() {
}
@Override public void test5() {
} }
|
最后,写一个测试类,观察接口中的三种方法,是如何调用的
1 2 3 4 5 6 7 8 9
| public class Test { public static void main(String[] args) { B b = new B(); b.test1(); A.test3(); } }
|
综上所述:JDK8对接口新增的特性,有利于对程序进行扩展。
4.5 接口的其他细节
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public class Test { public static void main(String[] args) { } }
interface A{ void test1(); } interface B{ void test2(); } interface C{}
interface D extends C, B, A{
}
class E implements D{ @Override public void test1() {
}
@Override public void test2() {
} }
|
接口除了上面的多继承特点之外,在多实现、继承和实现并存时,有可能出现方法名冲突的问题,需要了解怎么解决(仅仅只是了解一下,实际上工作中几乎不会出现这种情况)
1 2 3 4
| 1.一个接口继承多个接口,如果多个接口中存在相同的方法声明,则此时不支持多继承 2.一个类实现多个接口,如果多个接口中存在相同的方法声明,则此时不支持多实现 3.一个类继承了父类,又同时实现了接口,父类中和接口中有同名的默认方法,实现类会有限使用父类的方法 4.一个类实现类多个接口,多个接口中有同名的默认方法,则这个类必须重写该方法。
|
综上所述:一个接口可以继承多个接口,接口同时也可以被类实现。
4.6 左接口右实现类:
1
| List<Integer> list = new ArrayList<>();
|
声明左边为接口,右边为实现类,通常是指在变量声明中,左侧的类型是一个接口类型,而右侧是该接口的具体实现类的实例。这种方式的主要目的是为了提高代码的灵活性和可维护性。
注意:此时的list对象只能使用List接口中定义的方法(在ArrayList中实现的),而不能使用ArrayList中的别的方法,因为myList
被声明为 List<String>
类型
day03——面向对象高级
各位同学,前面两天我们已经把面向对象最主要的内容学习完了,剩下的这些语法知识学完,那么Java语法知识就算全齐活了。
今天学习的内容同学们学习起来会更轻松一些,有一些语法知识只需要了解一下就可以了,因为实际工作用得并不多。
我们先来了解第一个语法知识,内部类。
一、内部类
内部类是类中的五大成分之一(成员变量、方法、构造器、内部类、代码块),如果一个类定义在另一个类的内部,这个类就是内部类。
当一个类的内部,包含一个完整的事物,且这个事物没有必要单独设计时,就可以把这个事物设计成内部类。
比如:汽车、的内部有发动机,发动机是包含在汽车内部的一个完整事物,可以把发动机设计成内部类。
1 2 3 4 5 6
| public class Car{ public class Engine{ } }
|
内部类有四种形式,分别是成员内部类、静态内部类、局部内部类、匿名内部类。
我们先来学习成员内部类
1.1 成员内部类
成员内部类就是类中的一个普通成员,类似于成员变量、成员方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
| 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); System.out.println(a);
int age = 77; System.out.println(age); System.out.println(this.age); System.out.println(Outer.this.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; } } }
|
成员内部类如何创建对象,格式如下
1 2 3 4
| Outer.Inner in = new Outer().new Inner();
in.test();
|
总结一下内部类访问成员的特点
- 既可以访问内部类成员、也可以访问外部类成员
- 如果内部类成员和外部类成员同名,可以使用
类名.this.成员
区分
1.2 静态内部类
静态内部类,其实就是在成员内部类的前面加了一个static关键字。静态内部类属于外部类自己持有。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Outer { private int age = 99; public static String schoolName="黑马";
public static class Inner{ public void test(){ System.out.println(schoolName); } } }
|
静态内部类创建对象时,需要使用外部类的类名调用。
1 2 3
| Outer.Inner in = new Outer.Inner(); in.test();
|
1.3 局部内部类
局部内部类是定义在方法中的类,和局部变量一样,只能在方法中有效。所以局部内部类的局限性很强,一般在开发中是不会使用的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Outer{ public void test(){ class Inner{ public void show(){ System.out.println("Inner...show"); } } Inner in = new Inner(); in.show(); } }
|
1.4 匿名内部类
1.4.1 认识匿名内部类,基本使用
接下来学习一种在实际开发中用得最多的一种内部类,叫匿名内部类。相比于前面几种内部类,匿名内部类就比较重要的。
我们还是先认识一下什么是匿名内部类?
匿名内部类是一种特殊的局部内部类;所谓匿名,指的是程序员不需要为这个类声明名字。
下面就是匿名内部类的格式:
1 2 3 4
| new 父类/接口(参数值){ @Override 重写父类/接口的方法; }
|
匿名内部类本质上是一个没有名字的子类对象、或者接口的实现类对象。
比如,先定义一个Animal抽象类,里面定义一个cry()方法,表示所有的动物有叫的行为,但是因为动物还不具体,cry()这个行为并不能具体化,所以写成抽象方法。
1 2 3
| public abstract class Animal{ public abstract void cry(); }
|
接下来,我想要在不定义子类的情况下创建Animal的子类对象,就可以使用匿名内部类
1 2 3 4 5 6 7 8 9 10 11 12 13
| public class Test{ public static void main(String[] args){ Animal a = new Animal(){ @Override public void cry(){ System.out.println("猫喵喵喵的叫~~~"); } } a.eat(); } }
|
需要注意的是,匿名内部类在编写代码时没有名字,编译后系统会为自动为匿名内部类生产字节码,字节码的名称会以外部类$1.class
的方法命名

匿名内部类的作用:简化了创建子类对象、实现类对象的书写格式。
1.4.2 匿名内部类的应用场景
学习完匿名内部类的基本使用之后,我们再来看一下匿名内部类在实际中的应用场景。其实一般我们会主动的使用匿名内部类。
只有在调用方法时,当方法的形参是一个接口或者抽象类,为了简化代码书写,而直接传递匿名内部类对象给方法。这样就可以少写一个类。比如,看下面代码
1 2 3
| public interface Swimming{ public void swim(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| public class Test{ public static void main(String[] args){ Swimming s1 = new Swimming(){ @Override public void swim(){ System.out.println("狗刨飞快"); } }; go(s1); Swimming s1 = new Swimming(){ @Override public void swim(){ System.out.println("猴子游泳也还行"); } }; go(s1); } public static void go(Swimming s){ System.out.println("开始~~~~~~~~"); s.swim(); System.out.println("结束~~~~~~~~"); } }
|
二、枚举
2.1 认识枚举
2.1.1 认识枚举、枚举的原理
同学们,接下来我们学习一个新的知识点,枚举。枚举是我们以后在项目开发中偶尔会用到的知识。话不多说,我们还是先来认识一下枚举。
枚举是一种特殊的类,它的格式是:
1 2 3
| public enum 枚举类名{ 枚举项1,枚举项2,枚举项3; }
|
其实枚举项就表示枚举类的对象,只是这些对象在定义枚举类时就预先写好了,以后就只能用这几个固定的对象。
我们用代码演示一下,定义一个枚举类A,在枚举类中定义三个枚举项X, Y, Z
1 2 3
| public enum A{ X,Y,Z; }
|
想要获取枚举类中的枚举项,只需要用类名调用就可以了
1 2 3 4 5 6 7 8
| public class Test{ public static void main(String[] args){ A a1 = A.X; A a2 = A.Y; A a3 = A.Z; } }
|
刚才说,枚举项实际上是枚举类的对象,这一点其实可以通过反编译的形式来验证(需要用到反编译的命令,这里不能直接将字节码拖进idea反编译)

我们会看到,枚举类A是用class定义的,说明枚举确实是一个类,而且X,Y,Z都是A类的对象;而且每一个枚举项都是被public static final
修饰,所以被可以类名调用,而且不能更改。
2.1.2 枚举深入
既然枚举是一个类的话,我们能不能在枚举类中定义构造器、成员变量、成员方法呢?答案是可以的。来看一下代码吧
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| public enum A{ X,Y,Z("张三"); public A(){ } private String name; public A(String name){ this.name=name; } public String getName(){ return name; } ... }
|
虽然枚举类中可以像类一样,写一些类的其他成员,但是一般不会这么写,如果你真要这么干的话,到不如直接写普通类来的直接。
2.2 枚举的应用场景
刚才我们认识了一下什么是枚举,接下来我们看一下枚举在实际中的运用,枚举的应用场景是这样的:枚举一般表示一组信息,然后作为参数进行传输。
我们来看一个案例。比如我们现在有这么一个应用,用户进入应用时,需要让用户选择是女生、还是男生,然后系统会根据用户选择的是男生,还是女生推荐不同的信息给用户观看。

这里我们就可以先定义一个枚举类,用来表示男生、或者女生
1 2 3
| public enum Constant{ BOY,GRIL }
|
再定义一个测试类,完成用户进入系统后的选择
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| 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; } } }
|
最终再总结一下枚举的应用场景:枚举一般表示几个固定的值,然后作为参数进行传输。
三、泛型
3.1 认识泛型
所谓泛型指的是,在定义类、接口、方法时,同时声明了一个或者多个类型变量(如:),称为泛型类、泛型接口、泛型方法、它们统称为泛型。
比如我们前面学过的ArrayList类就是一个泛型类,我们可以打开API文档看一下ArrayList类的声明。

ArrayList集合的设计者在定义ArrayList集合时,就已经明确ArrayList集合时给别人装数据用的,但是别人用ArrayList集合时候,装什么类型的数据他不知道,所以就用一个<E>
表示元素的数据类型。
当别人使用ArrayList集合创建对象时,new ArrayList<String>
就表示元素为String类型,new ArrayList<Integer>
表示元素为Integer类型。

我们总结一下泛型的作用、本质:
3.2 自定义泛型类
接下来我们学习一下自定义泛型类,但是有一些话需要给大家提前交代一下:泛型类,在实际工作中一般都是源代码中写好,我们直接用的,就是ArrayList这样的,自己定义泛型类是非常少的。
自定义泛型类的格式如下
1 2 3 4
| public class 类名<T,W>{ }
|
接下来,我们自己定义一个MyArrayList泛型类,模拟一下自定义泛型类的使用。注意这里重点仅仅只是模拟泛型类的使用,所以方法中的一些逻辑是次要的,也不会写得太严谨。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
|
public class MyArrayList<E>{ private Object[] array = new Object[10]; private int index; public void add(E e){ array[index]=e; index++; } public E get(int index){ return (E)array[index]; } }
|
接下来,我们写一个测试类,来测试自定义的泛型类MyArrayList是否能够正常使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| public class Test{ public static void main(String[] args){ MyArrayList<String> list = new MyArrayList<>(); list.add("张三"); list.add("李四"); MyArrayList<Integer> list1 = new MyArrayList<>(); list.add(100); list.add(200); } }
|
关于自定义泛型类,你们把这个案例理解,对于初学者来说,就已经非常好了。
3.3 自定义泛型接口
在上一节中,我们已经学习了自定义泛型类,接下来我们学习一下泛型接口。泛型接口其实指的是在接口中把不确定的数据类型用<类型变量>
表示。定义格式如下:
1 2 3 4
| public interface 接口名<类型变量>{ }
|
比如,我们现在要做一个系统要处理学生和老师的数据,需要提供2个功能,保存对象数据、根据名称查询数据,要求:这两个功能处理的数据既能是老师对象,也能是学生对象。
首先我们得有一个学生类和老师类
1 2 3
| public class Teacher{
}
|
1 2 3
| public class Student{ }
|
我们定义一个Data<T>
泛型接口,T表示接口中要处理数据的类型。
1 2 3 4 5
| public interface Data<T>{ public void add(T t); public ArrayList<T> getByName(String name); }
|
接下来,我们写一个处理Teacher对象的接口实现类
1 2 3 4 5 6 7 8 9 10 11
|
public class TeacherData implements Data<Teacher>{ public void add(Teacher t){ } public ArrayList<Teacher> getByName(String name){ } }
|
接下来,我们写一个处理Student对象的接口实现类
1 2 3 4 5 6 7 8 9 10 11
|
public class StudentData implements Data<Student>{ public void add(Student t){ } public ArrayList<Student> getByName(String name){ } }
|
再啰嗦几句,在实际工作中,一般也都是框架底层源代码把泛型接口写好,我们实现泛型接口就可以了。
3.4 泛型方法
同学们,接下来我们学习一下泛型方法。下面就是泛型方法的格式
1 2 3
| public <泛型变量,泛型变量> 返回值类型 方法名(形参列表){ }
|
下图中在返回值类型和修饰符之间有定义的才是泛型方法。

接下我们看一个泛型方法的案例
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Test{ public static void main(String[] args){ String rs = test("test"); Dog d = test(new Dog()); } public static <T> test(T t){ return t; } }
|
3.5 泛型限定
接着,我们来学习一个泛型的特殊用法,叫做泛型限定。泛型限定的意思是对泛型的数据类型进行范围的限制。有如下的三种格式
- <?> 表示任意类型
- <? extends 数据类型> 表示指定类型或者指定类型的子类
- <? super 数据类型> 表示指定类型或者指定类型的父类
下面我们演示一下,假设有Car作为父类,BENZ,BWM两个类作为Car的子类,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| class Car{} class BENZ extends Car{} class BWN extends Car{}
public class Test{ public static void main(String[] args){ ArrayList<BWM> list1 = new ArrayList<>(); ArrayList<Benz> list2 = new ArrayList<>(); ArrayList<String> list3 = new ArrayList<>(); test1(list1); test1(list2); test1(list3); ArrayList<Car> list4 = new ArrayList<>(); ArrayList<BWM> list5 = new ArrayList<>(); test2(list4); test2(list5); ArrayList<Car> list6 = new ArrayList<>(); ArrayList<Object> list7 = new ArrayList<>(); test3(list6); test3(list7); } public static void test1(ArrayList<?> list){ } public static void test2(ArrayList<? extends Car> list){ } public static void test3(ArrayList<? super Car> list){ } }
|
3.6 泛型擦除
最后,关于泛型还有一个特点需要给同学们介绍一下,就是泛型擦除。什么意思呢?也就是说泛型只能编译阶段有效,一旦编译成字节码,字节码中是不包含泛型的。而且泛型只支持引用数据类型,不支持基本数据类型。
把下面的代码的字节码进行反编译

下面是反编译之后的代码,我们发现ArrayList后面没有泛型

四、常用API
各位同学,恭喜大家,到目前位置我们关于面向对象的语法知识就全部学习完了。接下来我们就可以拿着这些语法知识,去学习一个一个的API方法,掌握的API方法越多,那么Java的编程能力就越强。
API(Application Programming interface)意思是应用程序编程接口,说人话就是Java帮我们写好的一些程序,如:类、方法等,我们直接拿过来用就可以解决一些问题。

我们要学习那些API呢?把下面一种图中的所有类的常用方法学会了,那我们JavaSE进阶的课程就算你全学会了。

很多初学者给我反应的问题是,这些API一听就会,但是就是记住不!送同学们一句话,
“千里之行始于足下,多记、多查、多些代码、孰能生巧!”

4.1 Object类
各位小伙伴,我们要学习的第一个API就是Object类。Object类是Java中所有类的祖宗类,因此,Java中所有类的对象都可以直接使用Object类中提供的一些方法。
按照下图的提示,可以搜索到你想要找的类

我们找到Object类的下面两个方法

我们先来学习toString()方法。
1 2 3
| public String toString() 调用toString()方法可以返回对象的字符串表示形式。 默认的格式是:“包名.类名@哈希值16进制”
|
假设有一个学生类如下
1 2 3 4 5 6 7 8 9
| public class Student{ private String name; private int age; public Student(String name, int age){ this.name=name; this.age=age; } }
|
再定义一个测试类
1 2 3 4 5 6
| public class Test{ public static void main(String[] args){ Student s1 = new Student("赵敏",23); System.out.println(s1.toString()); } }
|
打印结果如下

如果,在Student类重写toString()方法,那么我们可以返回对象的属性值,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Student{ private String name; private int age; public Student(String name, int age){ this.name=name; this.age=age; } @Override public String toString(){ return "Student{name=‘"+name+"’, age="+age+"}"; } }
|
运行测试类,结果如下

4.1.2 equals(Object o)方法
接下来,我们学习一下Object类的equals方法
1 2
| public boolean equals(Object o) 判断此对象与参数对象是否"相等"
|
我们写一个测试类,测试一下
1 2 3 4 5 6 7 8 9 10 11
| public class Test{ public static void main(String[] args){ Student s1 = new Student("赵薇",23); Student s2 = new Student("赵薇",23); System.out.println(s1.equals(s2)); System.out.println(s1==s2); } }
|
但是如果我们在Student类中,把equals方法重写了,就按照对象的属性值进行比较
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| public class Student{ private String name; private int age; public Student(String name, int age){ this.name=name; this.age=age; } @Override public String toString(){ return "Student{name=‘"+name+"’, age="+age+"}"; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false;
Student student = (Student) o;
if (age != student.age) return false; return name != null ? name.equals(student.name) : student.name == null; } }
|
再运行测试类,效果如下

总结一下Object的toString方法和equals方法
1 2 3 4 5 6 7
| public String toString() 返回对象的字符串表示形式。默认的格式是:“包名.类名@哈希值16进制” 【子类重写后,返回对象的属性值】 public boolean equals(Object o) 判断此对象与参数对象是否"相等"。默认比较对象的地址值,和"=="没有区别 【子类重写后,比较对象的属性值】
|
4.1.3 clone() 方法
接下来,我们学习Object类的clone()方法,克隆。意思就是某一个对象调用这个方法,这个方法会复制一个一模一样的新对象,并返回。
1 2
| public Object clone() 克隆当前对象,返回一个新对象
|
想要调用clone()方法,必须让被克隆的类实现Cloneable接口。如我们准备克隆User类的对象,代码如下
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public class User implements Cloneable{ private String id; private String username; private String password; private double[] scores;
public User() { }
public User(String id, String username, String password, double[] scores) { this.id = id; this.username = username; this.password = password; this.scores = scores; }
@Override protected Object clone() throws CloneNotSupportedException { return super.clone(); } }
|
接着,我们写一个测试类,克隆User类的对象。并观察打印的结果
1 2 3 4 5 6 7 8 9 10 11
| public class Test { public static void main(String[] args) throws CloneNotSupportedException { User u1 = new User(1,"zhangsan","wo666",new double[]{99.0,99.5}); User u2 = (User) u1.clone(); System.out.println(u2.getId()); System.out.println(u2.getUsername()); System.out.println(u2.getPassword()); System.out.println(u2.getScores()); } }
|
我们发现,克隆得到的对象u2它的属性值和原来u1对象的属性值是一样的。

上面演示的克隆方式,是一种浅克隆的方法,浅克隆的意思:拷贝出来的对象封装的数据与原对象封装的数据一模一样(引用类型拷贝的是地址值)。如下图所示

还有一种拷贝方式,称之为深拷贝,拷贝原理如下图所示

下面演示一下深拷贝User对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| public class User implements Cloneable{ private String id; private String username; private String password; private double[] scores;
public User() { }
public User(String id, String username, String password, double[] scores) { this.id = id; this.username = username; this.password = password; this.scores = scores; }
@Override protected Object clone() throws CloneNotSupportedException { User u = (User) super.clone(); u.scores = u.scores.clone(); return u; } }
|

4.2 Objects类
Objects是一个工具类,提供了一些方法可以对任意对象进行操作。主要方法如下

下面写代码演示一下这几个方法
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class Test{ public static void main(String[] args){ String s1 = null; String s2 = "itheima"; System.out.println(s1.equals(s2)); System.out.println(Objects.equals(s1,s2)); System.out.println(Objects.isNull(s1)); System.out.println(s1==null); System.out.println(Objects.nonNull(s2)); System.out.println(s2!=null); } }
|
4.3 基本类型包装类
同学们,接下来我们学习一下包装类。为什么要学习包装类呢?因为在Java中有一句很经典的话,万物皆对象。Java中的8种基本数据类型还不是对象,所以要把它们变成对象,变成对象之后,可以提供一些方法对数据进行操作。
Java中8种基本数据类型都用一个包装类与之对一个,如下图所示

我们学习包装类,主要学习两点:
- 创建包装类的对象方式、自动装箱和拆箱的特性;
- 利用包装类提供的方法对字符串和基本类型数据进行相互转换
4.2.1 创建包装类对象
我们先来学习,创建包装类对象的方法,以及包装类的一个特性叫自动装箱和自动拆箱。我们以Integer为例,其他的可以自己学,都是类似的。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| Integer a = new Integer(10);
Integer b = Integer.valueOf(10);
Integer c = 10;
int d = c;
ArrayList<Integer> list = new ArrayList<>();
list.add(100);
int e = list.get(0);
|
4.2.2 包装类数据类型转换
在开发中,经常使用包装类对字符串和基本类型数据进行相互转换。
- 把字符串转换为数值型数据:包装类.parseXxx(字符串)
1 2
| public static int parseInt(String s) 把字符串转换为基本数据类型
|
- 将数值型数据转换为字符串:包装类.valueOf(数据);
1 2
| public static String valueOf(int a) 把基本类型数据转换为
|
1 2 3 4 5 6 7 8 9 10 11 12 13
| String ageStr = "29"; int age1 = Integer.parseInt(ageStr);
String scoreStr = 3.14; double score = Double.prarseDouble(scoreStr);
Integer a = 23; String s1 = Integer.toString(a); String s2 = a.toString(); String s3 = a+""; String s4 = String.valueOf(a);
|