文章目录
- 1. 类变量和类方法
- 1.1 为什么需要类变量 / 静态变量 / static变量
- 1.2 类变量的内存布局
- 1.3 类变量基本介绍
- 1.4 类变量使用细节
- 1.5 类方法基本介绍
- 1.6 类方法的细节
- 2. 理解 main 方法语法
- 3. 代码块
- 3.1 代码块基本介绍
- 3.2 代码块细节问题(可以只看3.2.2)
- 3.2.1 静态代码块和普通代码块
- 3.2.2 创建对象时的调用顺序
- 3.3 代码块练习
- 4. 单例设计模式
- 4.1 什么是设计模式
- 4.2 单例模式
- 4.2.1 饿汉式单例模式
- 4.2.2 懒汉式单例模式
- 4.2.3 饿汉式与懒汉式的区别
- 5. final 关键字
- 5.1 final 基本介绍
- 5.2 final 使用细节
- 5.3 final 练习
- 6. 抽象类
- 6.1 为什么需要抽象类
- 6.2 抽象类的介绍
- 6.3 抽象类的细节问题
- 7. 模板设计模式(抽象类最佳实践)
- 7.1 基本介绍
- 7.2 模板设计模式能解决的问题
- 8. 接口
- 8.1 接口快速入门
- 8.2 基本介绍
- 8.3 接口使用细节
- 8.4 接口与继承的区别与联系
- 8.5 接口的多态
- 9. 内部类
- 9.1 基本介绍
- 9.2 局部内部类
- 9.3 匿名内部类
- 9.3.1 基于接口的匿名内部类
- 9.3.2 基于类的匿名内部类
- 9.3.3 匿名内部类细节
- 9.3.4 匿名内部类做实参直接传递
- 9.4 成员内部类
- 9.5 静态内部类
- 10. 本章练习
1. 类变量和类方法
1.1 为什么需要类变量 / 静态变量 / static变量
看一个问题:有一群小孩在玩堆雪人,不时有新的小孩加入,问:如何知道现在共有多少人在玩?编写程序解决。
public class ChildGame {
public static void main(String[] args) {
int count = 0;//统计有多少小孩加入了游戏
Child child1 = new Child("白骨精");
child1.join();
count++;
Child child2 = new Child("狐狸精");
child2.join();
count++;
Child child3 = new Child("老鼠精");
child3.join();
count++;
System.out.println("共有" + count + "个小孩加入了游戏...");
}
}
class Child {
private String name;
public Child(String name) {
this.name = name;
}
public void join() {
System.out.println(name + " 加入了游戏..");
}
}
输出结果:
白骨精 加入了游戏..
狐狸精 加入了游戏..
老鼠精 加入了游戏..
共有3个小孩加入了游戏...
由输出结果看,上面代码虽然按照预期解决了问题,但是存在以下缺陷:
- count是一个独立于对象的变量,很尴尬。
- 以后访问 count 很麻烦,没有使用到OOP(面向对象)。
如果设计一个 int count 表示总人数,我们在创建一个小孩时,就把 count 加 1,并且count 是所有对象共享的就 ok 了,这就要用类变量 / 静态变量来解决。
类变量 / 静态变量 最大的特点就是会被类的所有的对象实例共享。
引入类变量 / 静态变量后,上面的问题可以这样解决:
public class ChildGame {
public static void main(String[] args) {
Child child1 = new Child("白骨精");
child1.join();
child1.count++;
Child child2 = new Child("狐狸精");
child2.join();
child2.count++;
Child child3 = new Child("老鼠精");
child3.join();
child3.count++;
//类变量,可以通过类名来访问
System.out.println("共有" + Child.count + "个小孩加入了游戏...");
//用任意一个对象访问也可以
System.out.println("child1.count=" + child1.count);//3
System.out.println("child2.count=" + child2.count);//3
System.out.println("child3.count=" + child3.count);//3
}
}
class Child {
private String name;
public static int count = 0;
public Child(String name) {
this.name = name;
}
public void join() {
System.out.println(name + " 加入了游戏..");
}
}
输出结果:
白骨精 加入了游戏..
狐狸精 加入了游戏..
老鼠精 加入了游戏..
共有3个小孩加入了游戏...
child1.count=3
child2.count=3
child3.count=3
1.2 类变量的内存布局
在JDK8之前,类变量的内存布局如红色虚线所示,JDK8之后如绿色实线所示(大概是这样,可能不是很准确)。
不管类变量在哪里,可以确定的是:
(1)类变量可以被同一个类的所有对象共享。
(2)类变量在类加载的时候就生成了。
1.3 类变量基本介绍
类变量 / 静态(static)变量 / 静态属性: 是该类的所有对象共享的变量,任何一个该类的对象去访问它时,取到的都是相同的值,任何一个该类的对象去修改它时,修改的也是同一个变量。
定义语法:
(1)访问修饰符 static 数据类型 变量名;(推荐)
(2)static 访问修饰符 数据类型 变量名;
访问:
(1)类名 . 类变量名(推荐)
类变量是随着类的加载而创建,所以即使没有创建对象实例也可以访问。
(2)对象名 . 类变量名
静态变量访问修饰符的访问权限、范围与普通属性相同。
public class Test {
public static void main(String[] args) {
//通过类名.类变量名访问
System.out.println(A.name);
A a = new A();
//通过对象名.类变量名访问
System.out.println("a.name=" + a.name);
}
}
class A {
//类变量的访问,必须遵守相关的访问权限
//若用private修饰,其他类中就不能直接访问了
public static String name = "marry";
}
1.4 类变量使用细节
- 何时需要用类变量:当需要让某个类的所有对象共享一个变量时,就可以考虑使用类变量(静态变量)。比如:定义学生类,统计所有学生共交多少钱。Student(name,static fee)
- 类变量与实例变量(普通属性)区别:类变量是该类的所有对象共享的,而实例变量是每个对象独享的,带有static的变量称为类变量或静态变量,否则称为实例变量 / 普通变量 / 非静态变量。
- 类变量可以通过 “类名 . 类变量名” 或者 “对象名 . 类变量名” 来访问,但推荐前者。前提是满足访问修饰符的访问权限和范围。
- 实例变量不能通过 “类名 . 类变量名” 的方式访问。
- 类变量在类加载时就已经初始化,也就是说:只要类加载了,即使没有创建对象,也可以使用类变量。
- 类变量的生命周期:随类的加载而开始,随类的消亡而销毁,
1.5 类方法基本介绍
类方法也叫静态方法。
定义语法:
(1)访问修饰符 static 返回类型 方法名(){ }(推荐)
(2)static 访问修饰符 返回类型 方法名(){ }
调用:
(1)类名 . 类方法名(推荐)
(2)对象名 . 类方法名
前提:满足访问修饰符的访问权限和范围。
【例1】学生学费总和。
public class Test {
public static void main(String[] args) {
//创建2个学生对象,交学费
Stu tom = new Stu("tom");
Stu.payFee(100);//tom.payFee(100)也可以
Stu mary = new Stu("mary");
Stu.payFee(200);//mary.payFee(200)也可以
Stu.showFee();//当前总学费300
}
}
class Stu {
private String name;
private static double fee = 0;//静态变量,累积学费
public Stu(String name) {
this.name = name;
}
//当方法使用了 static 修饰后,该方法就是静态方法
//静态方法就可以访问静态属性/变量
public static void payFee(double fee) {
Stu.fee += fee;
}
public static void showFee() {
System.out.println("总学费有:" + Stu.fee);
}
}
- 当方法不涉及与对象相关的成员时,可以设计成静态方法。这样,不创建对象就能调用某个方法。在实际开发中,会将通用的方法设计成静态方法,比如:打印一维数组,冒泡排序,完成某个计算任务等。
- Java工具类,比如:Math类、Array类、Collection类中的方法,很多都是静态方法。
public class Test {
public static void main(String[] args) {
System.out.println(MyTools.calSum(10, 30));
System.out.println(Math.sqrt(9));
}
}
class MyTools {
public static double calSum(double n1, double n2) {
return n1 + n2;
}
//可以写出很多这样的工具方法...
}
1.6 类方法的细节
- 类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区。
- 类方法可以通过类名或对象名调用;普通方法和对象有关,只能通过对象名调用。
- 类方法中不允许使用和对象有关的关键字,比如 this 和 super。普通方法可以。
- 在本类中,类方法(静态方法)可以直接访问静态成员(变量/方法);非静态方法可以直接访问非静态成员或静态成员;如果类方法(静态方法)要访问非静态成员,就要先创建对象再访问。
- 在遵循访问权限的前提下,其他类中的方法(静态 / 非静态)都可以直接访问某个类的静态成员;其他类在创建非静态成员所在类的对象后,其中的方法才可以访问该非静态成员(其实就是初学时常用的那种先创建对象再访问成员)。
- 构造器也是非静态方法,可以访问非静态和静态成员。(但静态方法中可以调用构造器创建对象,所以构造器比较特殊)
- 静态方法可以被继承但不能被重写。
class D {
private int n1 = 100;
private static int n2 = 200;
public void say() {
}
public static void hi() {
//静态方法中不能使用this或super
//System.out.println(this.n1);
}
//类方法(静态方法)只能访问静态成员
public static void hello() {
System.out.println(n2);
System.out.println(D.n2);
//System.out.println(this.n2);//错误
hi();//正确
//say();//错误
}
//普通成员方法,可以访问访问静态成员和非静态成员
public void ok() {
//非静态成员
System.out.println(n1);
say();
//静态成员
System.out.println(n2);
hello();
}
}
//如果类方法(静态方法)非要访问非静态成员,要先创建对象再访问
class Person {
private static int total = 3;
private int id = 5;
public static void printVar(){
System.out.println(total);
Person person = new Person();
System.out.println(person.id);
}
}
public class Test {
public static void main(String[] args) {
Person.printVar();//3 5
}
}
练习:
【例1】看看下面代码有没有错误,如果有错误,就修改,看看输出什么?
class Person {
private int id;
private static int total = 0;
public static int getTotalPerson() {
//id++;//错误, 静态方法不能访问非静态变量
return total;
}
//构造器也是非静态方法,可以访问非静态和静态成员
public Person() {
total++;
id = total;
}
}
public class Test {
public static void main(String[] args) {
System.out.println(Person.getTotalPerson()); //0
Person p1 = new Person();
System.out.println(Person.getTotalPerson()); //1
}
}
【例2】下面代码有没有错误,如果有错误,就修改。修改后,total 等于多少?
class Person {
private int id;
private static int total = 0;
public static void setTotalPerson(int total){
// this.total = total;//错误,static方法不能使用this
//不能使用this,又要区分局部变量total和属性total
//就可以直接用类名的形式访问
Person.total = total;
}
public Person() {
total++;
id = total;
}
public static void printTotal(){
System.out.println(total);
}
}
public class Test {
public static void main(String[] args) {
Person.setTotalPerson(3);
new Person();
Person.printTotal();//total的值是 4
}
}
2. 理解 main 方法语法
解释 main 方法的形式:public static void main(String[] args)
- main 方法由虚拟机调用。
- 访问权限必须是 public:Java 虚拟机(JVM)需要调用类的 main() 方法。
- 方法必须是 static:Java 虚拟机在执行 main() 方法时不必创建对象。
- 返回值是void:Java 虚拟机调用 main() 方法不需要返回值,能执行成功就执行,不能成功就退出或异常终止。
- 该方法接收 String数组 类型的参数,该数组中保存 “执行Java命令时,传递给所运行的类的参数” 。
若要在idea中传参数,按下面步骤操作:
main()方法与普通static方法访问本类成员的规则一致:
- main()方法是static方法,可以直接调用所在类的静态方法或静态属性。
- main()方法不能直接访问该类中的非静态成员,必须创建该类的对象后,才能通过这个对象去访问类中的非静态成员。
public class Test {
private static String name = "Smith";
private int n1 = 10000;
public static void hi() {
System.out.println("Test 的 hi 方法");
}
public void cry() {
System.out.println("Test 的 cry 方法");
}
public static void main(String[] args) {
System.out.println(name);
hi();
Test test = new Test();
System.out.println(test.n1);
test.cry();
}
}
输出结果:
Smith
Test 的 hi 方法
10000
Test 的 cry 方法
3. 代码块
3.1 代码块基本介绍
代码块是什么:
- 代码化块又称为初始化块,属于类中的成员,类似于方法,将逻辑语句封装在方法体中,通过{}包围起来。
- 代码块只有方法体,不是通过对象或类显式调用,而是加载类或创建对象时隐式调用。
语法:
[修饰符]{
代码
};
- 修饰符可选,要写的话,也只能写 static。
- 代码块分为两类,使用static修饰的称为静态代码块,没有static修饰的,称为普通代码块/非静态代码块。
- 逻辑语句可以为任何逻辑语句(输入、输出、方法调用、循环、判断等)
- 最后的 “ ; ”号可以写上,也可以省略。
【例】下面的三个构造器都有相同的语句,这样代码很啰嗦。可以把相同语句,放入一个代码块中。在创建对象时,不管调用哪个构造器,都会先调用代码块的内容。
public class Test {
public static void main(String[] args){
Movie movie = new Movie("你好,李焕英");
System.out.println("===============");
Movie movie2 = new Movie("唐探 3", 100, "陈思诚");
}
}
class Movie {
private String name;
private double price;
private String director;
{
System.out.println("电影屏幕打开...");
System.out.println("广告开始...");
System.out.println("电影正式开始...");
};
public Movie(String name) {
System.out.println("Movie(String name) 被调用...");
this.name = name;
}
public Movie(String name, double price, String director) {
System.out.println("Movie(String name, double price, String director) 被调用...");
this.name = name;
this.price = price;
this.director = director;
}
}
输出结果:
电影屏幕打开...
广告开始...
电影正式开始...
Movie(String name) 被调用...
===============
电影屏幕打开...
广告开始...
电影正式开始...
Movie(String name, double price, String director) 被调用...
3.2 代码块细节问题(可以只看3.2.2)
3.2.1 静态代码块和普通代码块
- static 代码块也叫静态代码块,作用就是对类进行初始化。
静态代码块在类加载的时候执行;
无论为某个类创建多少个对象,该类都只会加载一次;
所以,为某个类创建多个对象时,静态代码块只执行一次。
public class Test {
public static void main(String[] args){
DD dd = new DD();
DD dd2 = new DD();
}
}
class DD {
static {//静态代码块
System.out.println("DD 的静态代码块被执行...");
}
}
输出结果:
DD 的静态代码块被执行...
- 类什么时候被加载 ?
①
public class Test {
public static void main(String[] args){
//1. 创建对象实例时
AA aa = new AA();
}
}
class AA {
static {//静态代码块
System.out.println("AA 的静态代码块被执行...");
}
}
输出结果:
AA 的静态代码块被执行...
②
public class Test {
public static void main(String[] args){
//创建子类对象时,父类也会被加载。先加载父类,再加载子类
AA aa2 = new AA();
}
}
class BB {
static {//静态代码块
System.out.println("BB 的静态代码块被执行...");
}
}
class AA extends BB {
static {//静态代码块
System.out.println("AA 的静态代码块被执行...");
}
}
输出结果:
BB 的静态代码块被执行...
AA 的静态代码块被执行...
③ 使用类的静态成员时(静态属性 / 方法)
【例1】
public class Test {
public static void main(String[] args){
//使用类的静态成员时(静态属性,静态方法)
System.out.println(Cat.n1);
}
}
class Cat{
public static int n1 = 999;//静态属性
static {//静态代码块
System.out.println("Cat 的静态代码块被执行...");
}
}
输出结果:
Cat 的静态代码块被执行...
999
【例2】
public class Test {
public static void main(String[] args){
//使用类的静态成员时(静态属性,静态方法)
System.out.println(Cat.n1);
}
}
class Animal {
static {//静态代码块
System.out.println("Animal 的静态代码块被执行...");
}
}
class Cat extends Animal {
public static int n1 = 999;//静态属性
static {//静态代码块
System.out.println("Cat 的静态代码块被执行...");
}
}
输出结果:
Animal 的静态代码块被执行...
Cat 的静态代码块被执行...
999
- 普通的代码块,在创建对象实例时,会被隐式地调用。每创建一次对象,就会调用一次代码块。
对普通代码块的理解:
(1) 相当于构造器的补充机制,可以做初始化操作。
(2) 如果多个构造器中都有重复语句,可以抽取到代码块中,提高重用性。
public class Test {
public static void main(String[] args){
DD dd = new DD();
DD dd2 = new DD();
}
}
class DD {
static {//静态代码块
System.out.println("DD 的静态代码块被执行...");
}
{//普通代码块
System.out.println("DD 的普通代码块被执行...");
}
}
输出结果:
DD 的静态代码块被执行...
DD 的普通代码块被执行...
DD 的普通代码块被执行...
- 如果只是使用类的静态成员,普通代码块不会执行。
(普通代码块被调用只与是否创建对象有关,与类加载无关。可以这样简单理解:普通代码块是构造器的补充,构造器被调用时,普通代码块才被调用)
public class Test {
public static void main(String[] args){
System.out.println(DD.n1);
}
}
class DD {
public static int n1 = 8888;
static {//静态代码块
System.out.println("DD 的静态代码块被执行...");
}
{//普通代码块
System.out.println("DD 的普通代码块被执行...");
}
}
输出结果:
DD 的静态代码块被执行...
8888
- 小结:
static代码块是在类加载时调用的,只会调用一次。(类加载的3种情况要记住)
普通代码块是在创建对象时调用的,每创建一次对象,就调用一次普通代码块。
3.2.2 创建对象时的调用顺序
创建一个对象时,在一个类中的调用顺序是:
① 调用静态代码块和静态属性初始化(两者优先级相同,若有多个静态代码块 / 属性初始化,则按定义顺序调用)。
② 调用普通代码块和普通属性的初始化(两者优先级相同,若有多个普通代码块 / 属性初始化,则按定义顺序调用)。
③ 调用构造方法。
public class Test {
public static void main(String[] args){
A a = new A();
}
}
class A {
public A(){
System.out.println("构造器 被调用...");
}
//普通属性的显式初始化在创建对象时、构造器初始化之前,普通代码块的调用也是在创建对象时、构造器执行之前
//普通属性n2在前,所以先被初始化,于是getN2()被调用
//普通代码块在后,所以后被调用
private int n2 = getN2();
{ //普通代码块
System.out.println("普通代码块 被调用...");
}
public int getN2() {
System.out.println("getN2 被调用...");
return 100;
}
//静态变量在类加载时初始化,静态代码块也是在类加载时被调用
//但静态变量n1在前,所以先被初始化,于是getN1()被调用
//静态代码块在后,所以后被调用
private static int n1 = getN1();
static { //静态代码块
System.out.println("静态代码块 被调用...");
}
public static int getN1() {
System.out.println("getN1 被调用...");
return 100;
}
}
输出结果:
getN1 被调用...
静态代码块 被调用...
getN2 被调用...
普通代码块 被调用...
构造器 被调用...
构造器的最前面其实隐含了 super 和调用普通代码块。静态相关的代码块、属性初始化,在类加载时,就执行完毕,因此优先于构造器和普通代码块执行。
输出结果:
AAA 的普通代码块...
AAA() 构造器被调用...
BBB 的普通代码块...
BBB() 构造器被调用...
创建子类对象时(继承关系),静态代码块,静态属性初始化,普通代码块,普通属性初始化,构造方法的调用顺序如下:
① 父类的静态代码块和静态属性(优先级一样,按定义顺序执行)
② 子类的静态代码块和静态属性(优先级一样,按定义顺序执行)
③ 父类的普通代码块和普通属性初始化(优先级一样, 按定义顺序执行)
④ 父类的构造方法
⑤ 子类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
⑥ 子类的构造方法
【例】
(1)加载类,由于B02有父类A02:
所以先加载父类A02:初始化静态属性(getVal01被调用)、调用静态代码块
再加载子类B02:初始化静态属性(getVal03被调用)、调用静态代码块
(2)创建B02对象:
进入B02的构造器 ➡ super()进入父类A02的构造器,在父类A02的构造器中:
super()调用Object类的构造器;
初始化普通属性(getVal02被调用)、调用普通代码块;
执行A02构造器中的剩余语句。
父类构造器执行完毕后,回到子类B02构造器,执行super()后面的语句:
初始化普通属性(getVal04被调用)、调用普通代码块;
执行B02构造器中的剩余语句。
public class Test {
public static void main(String[] args){
new B02();
//注:()序号为执行顺序
}
}
class A02 { //父类
private static int n1 = getVal01();
static {
System.out.println("A02 的一个静态代码块..");//(2)
}
{
System.out.println("A02 的第一个普通代码块..");//(5)
}
public int n3 = getVal02();//普通属性的初始化
public static int getVal01() {
System.out.println("getVal01");//(1)
return 10;
}
public int getVal02() {
System.out.println("getVal02");//(6)
return 10;
}
public A02() {//构造器
//下面注释的语句会执行但隐藏了
//super()
//初始化普通属性、调用普通代码块
System.out.println("A02 的构造器");//(7)
}
}
class B02 extends A02 {
private static int n3 = getVal03();
static {
System.out.println("B02 的一个静态代码块..");//(4)
}
public int n5 = getVal04();
{
System.out.println("B02 的第一个普通代码块..");//(9)
}
public static int getVal03() {
System.out.println("getVal03");//(3)
return 10;
}
public int getVal04() {
System.out.println("getVal04");//(8)
return 10;
}
public B02() {//构造器
//下面注释的语句会执行但隐藏了
//super()
//初始化普通属性、调用普通代码块
System.out.println("B02 的构造器");//(10)
}
}
输出结果:
getVal01
A02 的一个静态代码块..
getVal03
B02 的一个静态代码块..
A02 的第一个普通代码块..
getVal02
A02 的构造器
getVal04
B02 的第一个普通代码块..
B02 的构造器
在本类中,静态代码块只能直接访问静态成员(静态属性 / 方法),若要访问非静态成员可以先创建对象*;普通代码块可以直接访问任意成员。(静态代码块很多用法与静态方法相同)
【注】静态代码块在类加载的时候执行,那时候还没有创建对象、没有初始化非静态成员,所以要先创建对象才能访问非静态成员。
class C02 {
private int n1 = 100;
private static int n2 = 200;
private void m1() {
}
private static void m2() {
}
static {
//静态代码块,只能调用静态成员
//System.out.println(n1);错误
System.out.println(n2);//ok
//m1();//错误
m2();
}
{//普通代码块,可以使用任意成员
System.out.println(n1);
System.out.println(n2);//ok
m1();
m2();
}
}
3.3 代码块练习
【例】下面的代码输出什么?
class Person {
public static int total;
static {
total = 100;
System.out.println("in static block!");
}
}
public class Test {
public static void main(String[] args) {
System.out.println("total = "+ Person.total);
System.out.println("total = "+ Person.total);
}
}
分析:
访问 Person.total 前先加载Person类:初始化静态变量、调用静态代码块(输出"in static block!")
Person类加载完成后,输出Person.total(100)
再次输出Person.total前,不再加载Person类,直接输出Person.total(100)
输出结果:
in static block!
total = 100
total = 100
4. 单例设计模式
4.1 什么是设计模式
(1)是静态方法和属性的经典使用。
(2)设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格以及解决问题的思考方式。设计模式就像是经典的棋谱,不同的棋局,我们用不同的棋谱,免去我们自己再思考和摸索。
4.2 单例模式
单例设计模式: 单例就是单个的实例。类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
单例模式有两种方式: (1)饿汉式 (2)懒汉式
饿汉式和懒汉式单例模式的实现步骤:
(1)构造器私有化,防止直接 new
(2)类的内部创建对象
(3)向外暴露一个静态的公共方法,getInstance
(4)代码实现
4.2.1 饿汉式单例模式
public class Test {
public static void main(String[] args) {
System.out.println("n1="+GirlFriend.n1);
GirlFriend instance = GirlFriend.getInstance();
System.out.println(instance);
//因为类只加载一次,所以getInstance得到的仍是第一次创建对象前类加载时创建的对象
GirlFriend instance2 = GirlFriend.getInstance();
System.out.println(instance2);
System.out.println(instance == instance2);//true
}
}
class GirlFriend {
public static int n1 = 999;
private String name;
//为了能够在静态方法getInstance中,直接访问属性(返回gf对象)
//需要将其修饰为static
//类一加载,就初始化static属性,就创建对象(饿:还没等到要用就自己创建了)
private static GirlFriend gf = new GirlFriend("小红红");//(1)
//将构造器私有化,其他类中不能直接调用构造器,从而不能直接创建对象
private GirlFriend(String name) {//(2)
System.out.println("构造器被调用.");
this.name = name;
}
//在其他类中不创建对象就直接调用该方法,返回该类中创建的gf对象
//所以方法应该是static的
public static GirlFriend getInstance() {//(3)
return gf;
}
@Override
public String toString() {
return "GirlFriend{" +
"name='" + name + '\'' +
'}';
}
}
输出结果:
构造器被调用.
n1=999
GirlFriend{name='小红红'}
GirlFriend{name='小红红'}
true
单例模式中的对象通常都是重量级的对象,可能存在创建了但是没有使用的情况。
若上面代码的GirlFriend类中有public static int n1 = 100,main方法中只有这样一条语句:System.out.println(GirlFriend.n1),则GirlFriend.n1会导致GirlFriend类加载,从而创建了该类的对象,但是之后却没有用到,造成资源浪费。
于是有了懒汉式单例模式,只有使用时才会创建对象。
4.2.2 懒汉式单例模式
public class Test {
public static void main(String[] args) {
System.out.println("n1="+Cat.n1);
Cat instance = Cat.getInstance();
System.out.println(instance);
Cat instance2 = Cat.getInstance();
System.out.println(instance2);
System.out.println(instance == instance2);//true
}
}
class Cat {
public static int n1 = 999;
private String name;
//保证静态方法getInstance能够直接访问该静态属性
private static Cat cat ; //默认是 null //(1)
//构造器私有化,保证其他类不能直接利用该构造器创建对象
//懶汉式,只有当用户使用 getInstance 方法时,才返回cat对象(没有就创建),
//当再次调用时,会返回上次创建的cat对象。从而保证了单例
private Cat(String name) {//(2)
System.out.println("构造器调用...");
this.name = name;
}
//在其他类中不创建对象就直接调用该方法,返回该类中创建的cat对象
//所以方法应该是static的
public static Cat getInstance() {//类加载只加载静态方法,但不调用静态方法 //(3)
if(cat == null) {//如果还没有创建cat对象
cat = new Cat("小可爱");
}
return cat;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
'}';
}
}
输出结果:
n1=999
构造器调用...
Cat{name='小可爱'}
Cat{name='小可爱'}
true
4.2.3 饿汉式与懒汉式的区别
- 二者最主要的区别在于创建对象的时机不同:饿汉式是在类加载时就创建了对象实例,而懒汉式是在使用时才创建。
- 饿汉式不存在线程安全问题,懒汉式存在线程安全问题。(后面学习线程后,会完善)
多个线程同时执行到懒汉式的getInstance方法时,可能出现 ”检查发现对象还未创建,从而都去创建对象“ 的情况(最终保留最后创建的对象) - 饿汉式存在浪费资源的可能,原因是:如果用不到对象实例,饿汉式创建的对象就浪费了;懒汉式是使用时才创建,就不存在浪费的问题。
- 在我们JavaSE标准类中,java.lang.Runtime就是经典的单例模式:
5. final 关键字
5.1 final 基本介绍
有以下需求时,就会使用到final。
(1)不希望类被继承
(2)不希望父类的某个方法被子类覆盖/重写
(3)不希望类的某个属性的值被修改(常量)
(4)不希望某个局部变量被修改(常量)
5.2 final 使用细节
- final修饰的属性又叫常量,一般用 XX_XX_XX 来命名。
- final修饰的属性在定义时,必须赋初值,并且以后不能再修改,赋值可以在如下位置之一(多于一种会报错):
(1)定义时;
(2)在构造器中;
(3)在代码块中。
class AA {
public final double TAX_RATE = 0.08;//定义时赋值
public final double TAX_RATE2;
public final double TAX_RATE3;
public AA() {//构造器中赋值
TAX_RATE2 = 1.1;
}
{//在代码块赋值
TAX_RATE3 = 8.8;
}
}
- 如果final修饰的属性是静态的,则初始化的位置只能是:
(1)定义时;(2)在静态代码块。
不能在构造器中赋值,因为静态属性初始化是在类加载阶段,调用构造器是创建对象时,太晚了。
class AA {
public static final double TAX_RATE = 99.9;
public static final double TAX_RATE2 ;
static {
TAX_RATE2 = 3.3;
}
}
- final类不能继承,但是可以实例化对象。
public class Test {
public static void main(String[] args) {
CC cc = new CC();
}
}
final class CC {}
- 如果类不是 final 类,但是含有 final 方法,则该方法虽然不能重写,但是可以被继承。
public class Test {
public static void main(String[] args) {
new EE().cal();//输出"cal()方法"
}
}
class DD{
public final void cal(){
System.out.println("cal()方法");
}
}
class EE extends DD{}
- 一般来说,如果某个类已经是 final 类了,就没有必要再将方法修饰成 final 方法。
(因为 final 方法是为了不让子类重写,而 final 类连子类都没有,所以没必要) - final 不能修饰构造方法(即构造器)
- final 也可以修饰局部变量。
- final 和 static 往往搭配使用,效率更高。因为不会导致类加载(底层编译器做了优化处理)。final 与 static 顺序可颠倒。
public class Test {
public static void main(String[] args) {
System.out.println(BBB.n);
}
}
class BBB{
public final static int n = 10000;
static {
System.out.println("BBB静态代码块");
}
}
输出结果:
10000
若上面BBB类中的静态变量 n 不用 final 修饰,则会输出:
BBB静态代码块
10000
- 包装类(Integer,Double,Float,Boolean 等都是 final 类),String 也是 final 类。这些类都不能被继承。
5.3 final 练习
下面的代码是否有误,为什么?
public int addOne(final int x) {//形参可以用final修饰
++x; //错误,不能更改x的值
return x + 1; //可以,没有更改x的值
}
6. 抽象类
6.1 为什么需要抽象类
看一个问题:写一个父类Animal,其中有一个eat方法,子类要重写该方法。但这时,父类如果写了eat方法,可能没有用,或者不知道方法体里面应该写什么。就像下面代码:
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public void eat() {
System.out.println("这是一个动物,但是不知道吃什么..");
}
}
为了解决上述问题,将 eat 方法设计为抽象(abstract)方法。
抽象方法就是没有实现的方法,即:没有方法体。
当一个类中存在抽象方法时,需要将该类声明为 abstract 类。
一般来说,抽象类会被继承,由子类来实现抽象方法。
所以,上述代码可以修改为:
abstract class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public abstract void eat();
}
6.2 抽象类的介绍
- 用 abstract 关键字来修饰一个类时,这个类就叫抽象类。
访问修饰符 abstract 类名{} - 用 abstract 关键字来修饰一个方法时,这个方法就是抽象方法。
访问修饰符 abstract 返回类型 方法名(参数列表); //抽象方法没有方法体,没有大括号 - 抽象类的价值更多作用是在于设计,是设计者设计好后,让子类继承并实现
抽象类()。 - 抽象类,是考官比较爱问的知识点,在框架和设计模式使用较多。
6.3 抽象类的细节问题
- 抽象类不能被实例化。
- 抽象类可以没有 abstract 方法。
abstract class A {
public void hi() {
System.out.println("hi");
}
}
- 一旦类包含了 abstract 方法,则这个类必须声明为 abstract。
- abstract 只能修饰类和方法,不能修饰属性和其它。
- 抽象类可以有任意成员(因为抽象类还是类),比如:非抽象方法(有方法体的方法)、构造器、静态属性等等。
- 抽象方法不能有方法体。
- 如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它自己也声明为 abstract 类。
- 抽象方法不能用 private、final 和 static 修饰,因为这些关键字都与重写相违背。
private:私有方法不能重写(子类不能直接访问到)
final:修饰方法的目的就是不允许子类重写
static:与重写无关,不能与 abstract 一起
【例】编写一个Employee类,声明为抽象类,包含如下三个属性 name, id, salary.
提供必要的构造器和抽象方法:work()。 对于Manager类来说,他既是员工,还
具有奖金(bonus)的属性。请使用继承的思想,设计CommonEmployee类和
Manager类,要求类中提供必要的方法进行属性访问,实现work(), 提示 “经理/普
通员工+名字+工作中…
public class Test {
public static void main(String[] args) {
Manager jack = new Manager("jack", 999, 50000);
jack.setBonus(8000);
jack.work();
CommonEmployee tom = new CommonEmployee("tom", 888, 20000);
tom.work();
}
}
abstract class Employee {
private String name;
private int id;
private double salary;
public Employee(String name, int id, double salary) {
this.name = name;
this.id = id;
this.salary = salary;
}
//将 work 做成一个抽象方法
public abstract void work();
public String getName() {
return name;
}
}
class Manager extends Employee{
private double bonus;
public Manager(String name, int id, double salary) {
super(name, id, salary);
}
public void setBonus(double bonus) {
this.bonus = bonus;
}
@Override
public void work() {
System.out.println("经理 " + getName() + " 工作中...");
}
}
class CommonEmployee extends Employee{
public CommonEmployee(String name, int id, double salary) {
super(name, id, salary);
}
@Override
public void work() {
System.out.println("普通员工 " + getName() + " 工作中...");
}
}
输出结果:
经理 jack 工作中...
普通员工 tom 工作中...
7. 模板设计模式(抽象类最佳实践)
7.1 基本介绍
抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。
7.2 模板设计模式能解决的问题
(1)当功能内部一部分实现是确定的,一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。
(2)编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给其子类实现,就是一种模板模式。
【例】有多个类,完成不同的任务job,要求统计得到各自完成任务的时间。
一般解法:
public class Test1{
public static void main(String[] args)
{
AA aa = new AA();
aa.calculateTime();
BB bb = new BB();
bb.calculateTime();
}
}
class AA {
public void calculateTime() {
//开始的时间
long start = System.currentTimeMillis();
job();
//结束的时间
long end = System.currentTimeMillis();
System.out.println("任务执行时间 " + (end - start));
}
public void job() {
long num = 0;
for (long i = 1; i <= 10000000; i++) {
num += i;
}
}
}
class BB {
public void calculateTime() {
//开始的时间
long start = System.currentTimeMillis();
job();
//结束的时间
long end = System.currentTimeMillis();
System.out.println("任务执行时间 " + (end - start));
}
public void job() {
long num = 0;
for (long i = 1; i <= 8000000; i++) {
num *= i;
}
}
}
上述代码中,AA 类和 BB 类有重复的方法,代码冗余度较大,可以考虑将两个类中重复的方法抽取到一个父类(抽象类)中,不同的方法声明为抽象的,然后再去子类 AA 和 BB 中实现(重写)抽象方法。
public class Test {
public static void main(String[] args) {
AA aa = new AA();
aa.calculateTime();
BB bb = new BB();
bb.calculateTime();
}
}
abstract class Template { //抽象类-模板设计模式
public abstract void job();//抽象方法
public void calculateTime() {
//开始的时间
long start = System.currentTimeMillis();
//发起者是aa/bb(运行类型),所以会去执行AA/BB类中的job()方法
job(); //动态绑定机制
//结束的时间
long end = System.currentTimeMillis();
System.out.println("任务执行时间 " + (end - start));
}
}
class AA extends Template {
@Override
public void job() {//实现(重写)Template的抽象方法job
long num = 0;
for (long i = 1; i <= 10000000; i++) {
num += i;
}
}
}
class BB extends Template{
@Override
public void job() {//实现(重写)Template的抽象方法job
long num = 0;
for (long i = 1; i <= 8000000; i++) {
num *= i;
}
}
}
8. 接口
8.1 接口快速入门
看下面例子,直观地体会接口。
package com.hspedu.interface_;
public class Interface01 {
public static void main(String[] args) {
//Camera 实现了 UsbInterface
Camera camera = new Camera();
//Phone 实现了 UsbInterface
Phone phone = new Phone();
Computer computer = new Computer();
computer.work(phone);//把手机接入到计算机
System.out.println("===============");
computer.work(camera);//把相机接入到计算机
}
}
class Computer {
public void work(UsbInterface usbInterface){
usbInterface.start();
usbInterface.stop();
}
}
interface UsbInterface{
//规定接口的相关方法,即规范
void start();
void stop();
}
package com.hspedu.interface_;
public class Phone implements UsbInterface{
@Override
public void start() {
System.out.println("手机开始工作");
}
@Override
public void stop() {
System.out.println("手机停止工作");
}
}
package com.hspedu.interface_;
public class Camera implements UsbInterface{
@Override
public void start() {
System.out.println("相机开始工作");
}
@Override
public void stop() {
System.out.println("相机停止工作");
}
}
输出结果:
手机开始工作
手机停止工作
===============
相机开始工作
相机停止工作
8.2 基本介绍
(可以将接口理解为一个特殊的类。类实现接口也可达到代码重用的目的)
接口就是给出一些没有实现的方法,封装到一起。到某个类要使用的时候,再根据具体情况把这些方法写出来。语法:
interface 接口名{
//属性
//抽象方法(Jdk8及以后还有默认实现方法、静态方法)
}
class 类名 implements 接口名{
自己的属性;
自己的方法;
必须实现的接口的抽象方法;
}
Jdk 7.0 及以前,接口里的所有方法都没有方法体,即:都是抽象方法。
Jdk 8.0 及以后,接口里可以有静态方法,默认方法,即:接口中可以有方法的具体实现。
在接口中,抽象方法可以省略 abstract 关键字。
如果一个类 implements 接口,就要将该接口中的所有抽象方法都实现。
对初学者讲,理解接口的概念不算太难,难的是不知道什么时候使用接口,下面列举几个应用场景:
(1)现在要制造战斗机、直升机。专家只需把飞机需要的功能 / 规格定下来,然后让别的人具体实现即可。
(2)一个项目经理管理3个程序员,分别实现 MySql、Oracle、DB2 数据库的 connect、close。项目经理写好接口,程序员根据需求去具体实现接口中的方法。这样就能保证3个程序员写的方法有相同的方法名,便于管理。(下面以MySql、Oracle为例)
package com.hspedu.interface_;
public class Test {
public static void main(String[] args) {
MySqlDB mySqlDB = new MySqlDB();
m(mySqlDB);
OracleDB oracleDB = new OracleDB();
m(oracleDB);
}
public static void m(DBInterface db){
db.connect();
db.colse();
}
}
package com.hspedu.interface_;
public interface DBInterface {
//强制MySql、Oracle的连接和关闭方法都叫connect、close
public void connect();
public void colse();
}
package com.hspedu.interface_;
public class MySqlDB implements DBInterface{
@Override
public void connect() {
System.out.println("MySql已连接...");
}
@Override
public void colse() {
System.out.println("MySql已关闭...");
}
}
package com.hspedu.interface_;
public class OracleDB implements DBInterface{
@Override
public void connect() {
System.out.println("Oracle已连接...");
}
@Override
public void colse() {
System.out.println("Oracle已关闭...");
}
}
输出结果:
MySql已连接...
MySql已关闭...
Oracle已连接...
Oracle已关闭...
8.3 接口使用细节
- 接口不能被实例化。
(接口的本意是让类去实现它,然后我们再去创建该类的对象进行其他操作) - 接口中所有的方法都是 public 方法,接口中的抽象方法可以不用 abstract 修饰。
接口中的 void aaa() 实际上是 public abstract void aaa(),所以在接口中写 void aaa(){} 是错误的。 - 一个普通类实现接口,就必须将该接口的所有方法都实现。
- 抽象类实现接口,可以不用实现接口的方法。
就像身为抽象类的子类可以不去实现父类(也是抽象类)中的抽象方法一样。 - 一个类可以同时实现多个接口。
- 接口中的属性,只能是 final 的,而且是 public static final 修饰符。比如 int a=1 实际上是 public static final int a = 1(必须初始化)。
- 接口中属性的访向形式:接口名.属性名。
- 接口不能继承其它的类,但是可以继承多个别的接口。(类可以实现多个接口)
interface A extends B,C{} - 接口的修饰符只能是 public 和默认,这点和类的修饰符是一样的。
【例】
8.4 接口与继承的区别与联系
接口可以看作对单继承机制的补充。
下面代码中 LittleMonkey 只能拥有父类的功能:
class Monkey{
private String name;
public Monkey(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void climb(){
System.out.println(name+"会爬树");
}
}
class LittleMonkey extends Monkey {
public LittleMonkey(String name) {
super(name);
}
}
下面代码 LittleMonkey 除了可以拥有父类的功能,还可以拥有接口中的功能:
public class ExtendsVsInterface {
public static void main(String[] args) {
LittleMonkey wuKong = new LittleMonkey("孙悟空");
wuKong.climb();
wuKong.swimming();
wuKong.flying();
}
}
class Monkey{
private String name;
public Monkey(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void climb(){
System.out.println(name+"会爬树");
}
}
interface Fish{
void swimming();
}
interface Bird{
void flying();
}
class LittleMonkey extends Monkey implements Fish,Bird{
public LittleMonkey(String name) {
super(name);
}
@Override
public void swimming() {
System.out.println(getName()+"通过学习,可以像鱼儿一样游泳");
}
@Override
public void flying() {
System.out.println(getName()+"通过学习,可以像鸟儿一样飞翔");
}
}
输出结果:
孙悟空会爬树
孙悟空通过学习,可以像鱼儿一样游泳
孙悟空通过学习,可以像鸟儿一样飞翔
小结:当子类继承了父类,就自动的拥有父类的功能。如果子类需要扩展功能,就可以通过实现接口的方式扩展。可以理解为:“实现接口” 是对 Java 单继承机制的一种补充。
接口和继承解决的问题不同
继承的价值主要在于:解决代码的复用性和可维护性。
接口的价值主要在于:设计好各种规范(方法),让其它类去实现这些方法,不同的类实现方式不同。
接口比继承更加灵活
接口比继承更加灵话,继承要满足 is-a 的关系(人是一种动物),而接口只需满足like-a的关系(人像鸟一样飞翔)。
接口在一定程度上实现代码解耦[即:接口规范性+动态绑定机制](集合那里再讲)
8.5 接口的多态
接口引用可以指向实现了接口的类的对象。
- 多态参数
在前面 8.1 的案例中,Computer 类中的 work 方法的形参 UsbInterface usbInterface 既可以接收手机对象,又可以接收相机对象。 - 接收新创建的实现了接口的类的对象
public class InterfacePolyParameter {
public static void main(String[] args) {
IF if1 = new Monster();
IF if2 = new Car();
}
}
interface IF{}
class Monster implements IF{}
class Car implements IF{}
- 多态数组
演示一个案例:给Usb数组中,存放 Phone 和 相机对象,Phone类还有一个特有的方法cal(),请遍历Usb数组,如果是Phone对象,除了调用Usb 接口定义的方法外,还需要调用 Phone 特有方法 call。
public class InterfacePolyArr {
public static void main(String[] args) {
Usb[] usbs = new Usb[3];
usbs[0] = new Phone();
usbs[1] = new Pad();
usbs[2] = new Phone();
for (int i = 0; i < usbs.length; i++) {
System.out.println("--------------");
if (usbs[i] instanceof Phone){
((Phone) usbs[i]).call();//向下转型
}
usbs[i].work();//动态绑定机制
}
}
}
interface Usb{
void work();
}
class Phone implements Usb{
@Override
public void work() {
System.out.println("手机工作中");
}
public void call(){
System.out.println("手机打电话");
}
}
class Pad implements Usb{
@Override
public void work() {
System.out.println("Pad工作中");
}
}
接口存在多态传递现象。
public class InterfacePolyPass {
public static void main(String[] args) {
IG ig = new Teacher();
IH ih = new Teacher();
}
}
interface IH{}
interface IG extends IH{}
class Teacher implements IG{}
上例中,IG 接口继承了 IH 接口,Teacher类实现了 IG 接口,就相当于 Teacher 类也实现了 IH 接口。所以,若 IH 接口中有抽象方法,Teacher 类也应该去实现。
练习:看下面代码有没有错误,有就改正。
package com.hspedu;
interface A {
int x = 0;//等价 public static final int x = 0;
}
class B {
int x = 1;
}
class C extends B implements A {
public void pX() {
//System.out.println(x); //错误,原因不明确 x
//可以明确的指定 x
//访问接口的x使用A.x
//访问父类的x使用super.x
System.out.println(A.x + " " + super.x);
}
public static void main(String[] args) {
new C().pX();
}
}
9. 内部类
9.1 基本介绍
内部类: 一个类的内部又完整地嵌套了另一个类结构。被嵌套的类称为内部类(inner class),嵌套其他类的类称为外部类(outer class)。内部类是类的第五大成员。【类的五大成员:属性、方法、构造器、代码块、内部类】
内部类的特点: 可以直接访问私有属性,并体现类与类之间的包含关系。
注意:内部类是学习的难点,同时也是重点,后面看底层源码时,有大量的内部类。
基本语法:
class Outer{//外部类
class Inner{//内部类
}
}
class Other{//外部的其他类
}
内部类的分类:
- 定义在外部类局部位置上(比如方法内):
局部内部类(有类名)
匿名内部类(没有类名,重点) - 定义在外部类的成员位置上:
成员内部类(不用static修饰)
静态内部类(使用static修饰)
9.2 局部内部类
- 局部内部类是定义在外部类的局部位置(方法或代码块中),并且有类名。
- 不能添加访向修饰符,因为它的地位就是一个局部变量。局部变量不能使用修饰符。但是可以使用final修饰,因为局部变量也可以使用final。不用final修饰时,本外部类中的其他与该内部类同级的类可以继承该内部类;用final修饰时就不能再被继承。
- 作用域在定义它的方法或代码块中。
- 本质仍是一个类。
- 局部内部类可以直接访问外部类的成员,包含私有的。
- 外部类访问局部内部类的成员,要先创建对象(注意作用域),再访问。
- 外部其他类不能访问局部内部类(因为局部内部类地位是一个局部变量)。
- 如果外部类和局部内部类的成员重名,默认遵循就近原则,如果想访向外部类的成员,则可以使用(外部类名.this.成员)去访问。
public class InnerClassTest {
public static void main(String[] args) {
Outer outer = new Outer();
outer.m2();
}
}
class Outer{
private int n1 = 100;
private void m1(){
System.out.println("外部类的m1()被调用");
}
public void m2(){
final class Inner{//地位等同于局部变量
public void f1(){
//Outer.this本质就是外部类的对象, 即调用m2的对象
//如果外部类中的n1是静态的,可以直接用Outer.n1访问
System.out.println("内部类的n1=" + n1 + " 外部类的n1=" + Outer.this.n1);
m1();//内部类直接访问外部类中的成员
}
}
//class inner02 extends inner{}
//外部类先在内部类作用域内创建内部类的对象,再访问内部类的成员
Inner inner = new Inner();
inner.f1();
}
}
代码执行流程:
9.3 匿名内部类
匿名内部类: 匿名内部类定义在外部类的局部位置(比如方法中),并且没有类名。它的本质是一个类,同时还是一个对象。
匿名内部类的基本语法:
new 类或接口(参数列表){
类体
};
9.3.1 基于接口的匿名内部类
package com.hspedu.innerclass;
public class AnonymousInnerClass {
public static void main(String[] args) {
Outer02 outer02 = new Outer02();
outer02.method();
}
}
interface IA{
void cry();
}
class Outer02 {
public void method(){
//想使用IA接口,并创建对象
//传统方式是写一个类实现该接口,并创建对象
//但是,如果该类只使用一次,就可以用匿名内部类简化
//tiger的编译类型是IA,运行类型是匿名内部类
//IA tiger = new IA(){...}使jdk底层 先创建匿名内部类Outer04$1(外部类名+$+编号,编号是挨着排的,)
//然后马上创建了Outer04$1对象,并且把地址返回给tiger
//jdk底层创建的类:
//class Outer02$1 implements IA{
// @Override
// public void cry() {
// System.out.println("老虎在叫");
// }
//}
//匿名内部类使用一次,就不能再使用
IA tiger = new IA(){
@Override
public void cry() {
System.out.println("老虎在叫");
}
};
tiger.cry();//动态绑定机制
System.out.println(tiger.getClass());//获取运行类型(获取系统分配的匿名内部类名)
}
}
输出结果:
老虎在叫
class com.hspedu.innerclass.Outer02$1
9.3.2 基于类的匿名内部类
package com.hspedu.innerclass;
public class AnonymousInnerClass {
public static void main(String[] args) {
Outer02 outer02 = new Outer02();
outer02.method();
}
}
class Father{
public Father(String name) {//构造器
System.out.println("接收到 name=" + name);
}
public void test() {//方法
}
}
abstract class Animal{
abstract void eat();
}
class Outer02 {
public void method(){
//father的编译类型是Father,运行类型是匿名内部类Outer02$1
//jdk底层 先创建匿名内部类Outer04$1,然后马上创建了Outer04$1对象,并且把地址返回给father
//class Outer04$1 extends Father{
// @Override
// public void test() {
// System.out.println("匿名内部类重写了 test 方法");
// }
//}
注意("jack")参数列表会传递给Father类的构造器
Father father = new Father("jack"){
@Override
public void test() {
System.out.println("匿名内部类重写了test方法");
}
};
System.out.println("father 对象的运行类型=" + father.getClass());//Outer04$1
father.test();//动态绑定机制
System.out.println("===============================================");
//基于抽象类的匿名内部类 必须实现父类的抽象方法
Animal animal = new Animal(){
@Override
void eat() {
System.out.println("小狗吃骨头");
}
};
System.out.println("animal 对象的运行类型=" + animal.getClass());
animal.eat();//动态绑定机制
}
}
输出结果:
接收到 name=jack
father 对象的运行类型=class com.hspedu.innerclass.Outer02$1
匿名内部类重写了test方法
===============================================
animal 对象的运行类型=class com.hspedu.innerclass.Outer02$2
小狗吃骨头
9.3.3 匿名内部类细节
- 匿名内部类的语法比较奇特,因为匿名内部类既是一个类的定义,同时它本身也是一个对象。从语法上看,它既有定义类的特征,也有创建对象的特征,因此可以调用匿名内部类方法。
package com.hspedu.innerclass;
public class AnonymousInnerClass {
public static void main(String[] args) {
Outer02 outer02 = new Outer02();
outer02.method();
}
}
class Outer02{
public void method(){
Person person = new Person(){
@Override
public void ok(String name) {
System.out.println("子类(匿名内部类1)重写了父类的ok方法 "+name);
}
};
person.ok("jack");
//也可以这样调用
new Person(){
@Override
public void ok(String name) {
System.out.println("子类(匿名内部类2)重写了父类的ok方法 "+name);
}
}.ok("tom");//直接调用
}
}
class Person{
public void ok(String name) {
System.out.println("父类的ok方法 "+name);
}
}
输出结果:
子类(匿名内部类1)重写了父类的ok方法 jack
子类(匿名内部类2)重写了父类的ok方法 tom
- 匿名内部类可以直接访问外部类的所有成员,包含私有的。
package com.hspedu.innerclass;
public class AnonymousInnerClass {
public static void main(String[] args) {
Outer02 outer02 = new Outer02();
outer02.method();
}
}
class Outer02{
private int n1 = 100;
public void method(){
//类继承Person,并创建了该类对象,用父类Person的引用指向子类的对象
Person person = new Person(){
@Override
public void hi() {
//匿名内部类直接访问外部类的所有成员,包含私有的
System.out.println("n1 "+n1);
}
};
person.hi();
}
}
class Person{
public void hi() {
System.out.println("父类的hi方法");
}
}
- 不能添加访问修饰符,因为它的地位就是一个局部变量。
- 作用域仅在定义它的方法或代码块中。
- 匿名内部类只能使用一次
- 匿名内部类可以直接访问外部类的所有成员,包括私有的。
- 外部其他类不能访问匿名内部类,因为匿名内部类的地位是一个局部变量。
- 如果外部类和匿名内部类的成员重名时,匿名内部类访问的话,默认遵循就近原则。如果想访问外部类的成员,可以使用 “外部类名.this.成员” 去访问。
package com.hspedu.innerclass;
public class AnonymousInnerClass {
public static void main(String[] args) {
Outer02 outer02 = new Outer02();
outer02.method();
System.out.println("outer02的运行类型:"+outer02.getClass());
}
}
class Outer02{
private int n1 = 100;
public void method(){
//类继承Person,并创建了该类对象,用父类Person的引用指向子类的对象
Person person = new Person(){
private int n1 = 200;
@Override
public void hi() {
//Outer.this本质就是外部类的对象, 即调用method的对象
//如果外部类中的n1是静态的,可以直接用Outer.n1访问
System.out.println("内部类中的n1="+n1+" 外部类中的n1="+Outer02.this.n1);
System.out.println("Outer02.this的运行类型:"+Outer02.this.getClass());
}
};
person.hi();
}
}
class Person{
public void hi() {
System.out.println("父类的hi方法");
}
}
输出结果:
内部类中的n1=200 外部类中的n1=100
Outer02.this的运行类型:class com.hspedu.innerclass.Outer02
outer02的运行类型:class com.hspedu.innerclass.Outer02
9.3.4 匿名内部类做实参直接传递
传统方法:类实现接口 ➡ 创建对象 ➡ 传参
public class Test {
public static void main(String[] args) {
//传统方法
f1(new Picture());//创建对象和传参
}
//因为不知道匿名内部类的类名
//所以只能用IL类型的形参接收
public static void f1(IL il) {
il.show();
}
}
interface IL {
void show();
}
//编写一个类来实现IL => 编程领域 (硬编码)
class Picture implements IL {//实现接口
@Override
public void show() {
System.out.println("一副名画XX");
}
}
匿名内部类做实参直接传递:
public class Test {
public static void main(String[] args) {
//当做实参直接传递,简洁高效
f1(new IL() {
@Override
public void show() {
System.out.println("一副名画~~");
}
});
}
//因为不知道匿名内部类的类名
//所以只能用IL类型的形参接收
public static void f1(IL il) {
il.show();
}
}
interface IL {
void show();
}
练习:
有一个铃声接口Bell,里面有ring方法,
有一个手机类 Cellphone,具有闹钟功能 alarmclock,参数是 Bell 类型
测试手机类的闹钟功能,通过匿名内部类(对象)作为参数,打印:懒猪起床了
再传入另一个匿名内部类(对象),打印:小伙伴上课了
public class Test {
public static void main(String[] args) {
CellPhone cellPhone = new CellPhone();
cellPhone.alarmclock(new Bell(){
@Override
public void ring(){
System.out.println("懒猪起床了");
}
});
cellPhone.alarmclock(new Bell(){
@Override
public void ring(){
System.out.println("小伙伴上课了");
}
});
}
}
interface Bell {
void ring();
}
class CellPhone{
public void alarmclock(Bell bell){
bell.ring();
}
}
输出结果:
懒猪起床了
小伙伴上课了
9.4 成员内部类
说明:成员内部类定义在外部类的成员位置,并且没有static修饰
- 可以添加任意访问修饰符(public、protected、默认、private),因为它的地位就是一个成员。(前面学习的普通类只能用 public 和 默认 来修饰)
- 作用域和外部类的其他成员一样,为整个类体。
- 成员内部类可以直接访问外部类的成员,包含私有的。
- 外部类访问成员内部类中的成员:先创建对象,再访问。
public class Test {
public static void main(String[] args) {
Outer outer = new Outer();
outer.t1();
}
}
class Outer{
private int n1 = 10;
public String name = "张三";
private void hi() {
System.out.println("hi()方法...");
}
public class Inner{
private double sal = 99.8;
public void say(){
//可以直接访问外部类的所有成员,包括私有的
System.out.println("n1="+n1+" name="+name);
hi();
}
}
public void t1(){
//外部类访问成员内部类中的成员:先创建对象,再访问
Inner inner = new Inner();
inner.say();
//可以访问内部类的私有成员,因为在同一个类中
System.out.println("sal="+inner.sal);
}
}
- 外部其他类访问成员内部类中的成员。
public class Test {
public static void main(String[] args) {
//外部其他类,使用成员内部类的2种方式(记住就行)
Outer outer = new Outer();
//第1种方式:相当于把new Inner()当成outer的成员
Outer.Inner inner = outer.new Inner();
inner.say();
System.out.println("========================");
// 第2种方式:在外部类编写一个方法,返回Inner对象
Outer.Inner innerInstance = outer.getInnerInstance();
innerInstance.say();
}
}
class Outer{
private int n1 = 10;
public String name = "张三";
private void hi() {
System.out.println("hi()方法...");
}
public class Inner{
private double sal = 99.8;
public void say(){
//可以直接访问外部类的所有成员,包括私有的
System.out.println("n1="+n1+" name="+name);
hi();
}
}
//返回一个 Inner对象
public Inner getInnerInstance(){
return new Inner();
}
}
- 如果外部类和内部类的成员重名,在内部类访问时遵循就近原则,若想访向外部类的成员,可以使用 “外部类名.this.成员” 去访问。(与前面相同)
9.5 静态内部类
说明:静态内部类定义在外部类的成员位置,并且有static修饰。
- 可以添加任意访问修饰符(public、protected、默认、private),因为它的地位就是一个成员。(前面学习的普通类只能用 public 和 默认 来修饰)
- 作用域和外部类的其他成员一样,为整个类体。
- 静态内部类可以直接访向外部类的所有静态成员,包含私有的,但不能直接访问非静态成员。
- 外部类访问静态内部类中的成员:先创建对象,再访问。
public class Test {
public static void main(String[] args) {
Outer outer = new Outer();
outer.m();
}
}
class Outer{
private int n1 = 10;
public static String name = "张三";
private static void cry() {
System.out.println("cry()方法...");
}
static class Inner{
public void say(){
//可以直接访问外部类的所有静态成员,包括私有的
System.out.println("name="+name);
cry();
}
}
public void m(){
//外部类访问静态内部类中的成员:先创建对象,再访问
Inner inner = new Inner();
inner.say();
}
}
- 外部其他类访问静态内部类中的成员。
public class Test {
public static void main(String[] args) {
//外部其他类 使静态内部类
//方式1
//因为静态内部类,是可以通过类名直接访问(前提是满足访问权限)
Outer.Inner inner = new Outer.Inner();
inner.say();
System.out.println("============");
//方式2
//编写一个方法,可以返回静态内部类的对象实例
Outer.Inner inner2 = new Outer().getInner();
inner2.say();
System.out.println("============");
//方式2改进 返回对象的方法改成静态的
Outer.Inner inner3 = Outer.getInner_();
inner3.say();
}
}
class Outer{
private int n1 = 10;
public static String name = "张三";
private static void cry() {
System.out.println("cry()方法...");
}
static class Inner{
public void say(){
//可以直接访问外部类的所有静态成员,包括私有的
System.out.println("name="+name);
cry();
}
}
public Inner getInner() {//(方法2用)返回静态内部类的对象
return new Inner();
}
public static Inner getInner_() {//(方法2改进用)
return new Inner();
}
}
输出结果:
name=张三
cry()方法...
============
name=张三
cry()方法...
============
name=张三
cry()方法...
- 如果外部类和静态内部类的静态成员重名,在静态内部类中访问时,遵循就近原则,若想访问外部类的静态成员,则可以使用 “外部类名.成员” 去访问。
10. 本章练习
【例1】要求:
计算器接口具有 work 方法,功能为运算。
有一个手机类 Cellphone,定义方法 testWork 测试计算功能,并调用计算接口的 work 方法。
要求调用 Cellphone 对象的 testWork 方法,用上匿名内部类。
public class Test{
public static void main(String[] args) {
//匿名内部类把自己作为参数传给Cellphone类的testWork方法
//同时把运算数据也传给testWork方法
//使testWork方法利用该匿名内部类中的重写过的work方法运算
new Cellphone().testWork(new ICalculate() {
@Override
public double work(double a, double b) {
return a+b;
}
}, 2, 3);
}
}
interface ICalculate{
double work(double a, double b);
}
class Cellphone{
public void testWork(ICalculate calculate, double a, double b){
System.out.println(calculate.work(a, b));
}
}
【例2】要求:
有一个交通工具接口类 Vehicles,有 work 接口;
有 Horse 类和 Boat 类分别实现 Vehicles;
创建交通工具工厂类,有两个方法分别获得交通工具 Horse 和 Boat;
有 Person 类,有 name 和 vehicles 属性,在构造器中为两个属性赋值;
实例化 Person 对象 ”唐僧“,要求一般情况下用 Horse 作为交通工具,遇到大河时用 Boat作为交通工具。
public class Test {
public static void main(String[] args) {
Person tang = new Person("唐僧", new Horse());
tang.common();
tang.passRiver();
}
}
interface Vehicles{
void work();
}
class Horse implements Vehicles{
@Override
public void work() {
System.out.println("一般情况下,用Horse作为交通工具");
}
}
class Boat implements Vehicles{
@Override
public void work() {
System.out.println("过河时,用Boat作为交通工具");
}
}
class VehiclesFactory{//工厂
//饿汉式单例模式:唐僧全程只有一匹白龙马
private static Horse horse = new Horse();
private VehiclesFactory() {
}
public static Horse getHorse(){
return horse;
}
public static Boat getBoat(){
return new Boat();
}
}
class Person{
private String name;
private Vehicles vehicles;
public Person(String name, Vehicles vehicles) {
this.name = name;
this.vehicles = vehicles;
}
//注意体会编程思想:在这里把各个类联系起来
public void passRiver(){//过河时
if (!(vehicles instanceof Boat)){
vehicles = VehiclesFactory.getBoat();//向上转型
}
vehicles.work();
}
public void common(){//一般情况
if (!(vehicles instanceof Horse)){
vehicles = VehiclesFactory.getHorse();//向上转型
}
vehicles.work();
}
}
输出结果:
一般情况下,用Horse作为交通工具
过河时,用Boat作为交通工具
【例3】要求:
有一个Car类,有属性temperature(温度),
车内有Air(空调)类,有吹风的功能flow,Air会监视车内的温度,
如果温度超过40°就吹冷气。如果温度低于0°就吹暖气。
在关闭空调的状态下,实例化具有不同温度的Car对象,并调用空调的flow方法,测试空调吹的风是否正确。
public class Test {
public static void main(String[] args) {
Car car = new Car(50);
car.getAir().flow();
Car car1 = new Car(20);
car1.getAir().flow();
Car car2 = new Car(-2);
car2.getAir().flow();
}
}
class Car{
private double temperature;
public Car(double temperature) {
this.temperature = temperature;
}
class Air{
public void flow(){
if (temperature > 40){
System.out.println("温度超过40°,吹冷气");
}else if (temperature < 0){
System.out.println("温度低于0°,吹暖气");
}else {
System.out.println("温度适宜,空调关闭");
}
}
}
public Air getAir(){
return new Air();
}
}
输出结果:
温度超过40°,吹冷气
温度适宜,空调关闭
温度低于0°,吹暖气
【例4】要求:
创建一个Color枚举类,有RED, BLUE, BLACK, YELLOW, GREEN这五个枚举值 / 对象;
Color有三个属性redValue,greenValue,blueValue;
创建构造方法,参数包括这三个属性,每个枚举值都要给这三个属性赋值,三个属性对应的值分别是:red: 255,0,0 blue: 0,0,255 black: 0,0,0 yellow:255,255,0 green:0,255,0;
定义接口,里面有方法show,要求Color实现该接口,show方法中显示三个属性的值;
将枚举对象在switch语句中匹配使用。swtch的可以是枚举对象。
public class Test {
public static void main(String[] args) {
Color blue = Color.BLUE;
blue.show();
switch (blue){//swtch的可以是枚举对象
case RED:
System.out.println("匹配到红色");
break;
case BLUE:
System.out.println("匹配到蓝色");
break;
case BLACK:
System.out.println("匹配到黑色");
break;
case YELLOW:
System.out.println("匹配到黄色");
break;
case GREEN:
System.out.println("匹配到绿色");
break;
default:
System.out.println("未匹配到...");
}
}
}
interface IColor{
void show();
}
enum Color implements IColor{
RED(255,0,0),
BLUE(0,0,255),
BLACK(0,0,0),
YELLOW(255,255,0),
GREEN(0,255,0);
private int redValue;
private int greenValue;
private int blueValue;
private Color(int redValue, int greenValue, int blueValue) {
this.redValue = redValue;
this.greenValue = greenValue;
this.blueValue = blueValue;
}
@Override
public void show() {
System.out.println(redValue+"\t"+greenValue+"\t"+blueValue);
}
}
输出结果:
0 0 255
匹配到蓝色