8. Java面向对象编程(三)

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 的静态代码块被执行...
    
  • 类什么时候被加载 ?
    创建对象实例时(new)

    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
匹配到蓝色
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值