一、类变量/静态变量
1. 类变量引入
- 提出一个问题:
- 有一群小孩在玩堆雪人,不时有新的小孩加入,请问如何知道现在共有多少人在玩?,编写程序解决。
- 用传统的方法解决:(创建一个 Child 类)
- 思考:count 变量是一个独立于小孩对象存在的变量,在main 方法中定义该变量,如果想要在其他类中访问时,很麻烦。因此我们引出了类变量/静态变量的概念。
2. 类变量快速入门
- 思考: 在上题中,如果能在Child 类中设计一个变量 count 表示总人数,我们每次新建一个小孩对象时,就把 count 加 1,并且 count 是所有小孩对象共享的就可以了,所以我们可以使用类变量来解决。
- 代码实现:
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;
//定义一个变量 count ,是一个类变量(静态变量),static:静态
//该变量最大的特点就是会被Child 类的所有的对象实例共享
public static int count = 0;
public Child(String name) {
this.name = name;
}
public void join() {
System.out.println(name + " 加入了游戏..");
}
}
- 显而易见,将 count 设置为类变量后,解决了传统方法出现的问题,类变量可以通过类名被直接访问,在其他类中也可以方便地访问该变量了。
3. 类变量的内存布局
- 类对象的创建过程:
- 在第一次创建一个类的对象时,在内存的方法区中会加载该类的信息(只会加载一次),与此同时伴随着类的加载在堆内存中会生成一个类对象 (类对象不是类的一个具体对象,而是类的一个映射);
- 类变量也伴随着类的加载而生成,并存储在类对象的尾部;
- 只需记住两点:
(1)类变量是同一个类的所有对象共享的变量;
(2)类变量在类加载的时候就已经产生了,因此只需要通过类名便可以直接访问类变量。
4. 类变量基本概念
- 类变量:也叫静态变量/静态属性,是该类的所有对象共享的变量,任何一个该类的对象去访问它时,取到的都是相同的值;同样,任何一个该类的对象去修改它时,修改的也是同一个变量。
- 基本语法:
- 访问类变量:
- 注意:在访问类变量时,需要满足遵守修饰符的相关权限控制规则;类变量是随着类的加载而创建,因此即使没有创建对象实例也可以直接通过类名访问。
- 代码实现:
public class VisitStatic {
public static void main(String[] args) {
//类名.类变量名
//说明:类变量是随着类的加载而创建,所以即使没有创建对象实例也可以访问;
System.out.println(A.name);
A a = new A();
//通过对象名.类变量名
System.out.println("a.name=" + a.name);
}
}
class A {
// 类变量/静态成员变量/静态属性
public static String name = "韩顺平教育";
// 普通属性/普通成员变量/非静态属性/非静态成员变量/实例变量
private int num = 10;
}
- 补充:
变量的称谓:(称谓不同,但本质就是同一个东西)
- 类变量 = 静态成员变量 = 静态属性;
- 普通成员变量 = 普通属性 = 非静态属性 = 非静态成员变量 = 实例变量;
5. 类变量使用注意事项和细节
- 细节如下:
二、类方法/静态方法
1. 类方法基本概念
- 类方法:也叫静态方法,当一个普通的成员方法使用了 static 关键字修饰后,该方法就变成了静态方法。
- 基本语法:
- 类方法的使用:
(1) 推荐使用类名.类方法名来调用类方法。
(2) 如果我们希望不创建对象,也可以调用某个方法(即仅把该方法当做一个工具来使用),那就把该方法定义为一个静态方法。把这些工具方法都放进一个类,这个类可以当做我们的工具类来使用。
- 类方法经典的使用场景:
2. 类方法使用注意事项和细节
- 细节如下:
- 代码实现:
public class StaticMethodDetail {
public static void main(String[] args) {
D.hi();// 静态方法,可以直接通过类名调用
//非静态方法,不能通过类名调用
//D.say();, 错误,需要先创建对象,再调用
new D().say();// 通过对象调用
}
}
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); 错误,不能使用this关键字
hi();// 静态方法中可以访问静态方法
//say(); 错误,静态方法中不能访问非静态方法和成员
}
// 普通成员方法,既可以访问 非静态成员,也可以访问静态成员;
// 小结: 非静态方法可以访问 静态成员和非静态成员;
public void ok() {
//非静态成员
System.out.println(n1);
say();
//静态成员
System.out.println(n2);
hello();
}
}
- 注意:一切成员的访问前提是,要遵守访问修饰符限定的权限规则。
三、main 方法
1. 深入理解 main 方法
- main 方法是一种特殊的静态方法,它由Java虚拟机调用。
- 解释main 方法的形式:
2. 注意事项和细节
1) 在 main()方法中,我们可以直接调用 main 方法所在类的静态方法或静态属性。
2) 但是,不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员。
- 代码举例:
public class Main01 {
// 静态的变量/属性
private static String name = "韩顺平教育";
// 非静态的变量/属性
private int n1 = 10000;
// 静态方法
public static void hi() {
System.out.println("Main01的 hi 方法");
}
// 非静态方法
public void cry() {
System.out.println("Main01的 cry 方法");
}
public static void main(String[] args) {
// 1. 静态方法main 可以访问本类的静态成员
// 可以直接使用 name,和 hi 方法;
System.out.println("name=" + name);
hi();
// 2. 静态方法main 不可以访问本类的非静态成员
// System.out.println("n1=" + n1); 错误
// cry(); 错误
//3. 静态方法 main 要访问本类的非静态成员,需要先创建对象 , 再用对象调用
Main01 main01 = new Main01();
// 可以使用非静态成员了;
System.out.println(main01.n1);
main01.cry();
}
}
四、代码块
1. 代码块基本概念
- 代码块:又称为初始化块,属于类中的成员【即是类的一部分】,类似于方法,将逻辑语句封装在方法体中,用 {} 包围起来。
- 基本用法:
- 代码块的理解:
(1)相当于另外一种形式的构造器(是对构造器的补充机制),可以做初始化操作;
(2)使用场景,如果多个构造器中都有重复的语句,可以抽取到代码块中,提高代码的复用性,相当于对构造器的重载;
(3)代码块调用的顺序优先于构造器。
二、代码块使用注意事项和细节
- 注意事项:
1. 类什么时候被加载
(1)创建类的对象实例时(new),类会被加载。
(2)存在继承关系,当创建子类对象实例时,其所有上级父类也会被加载;并且父类先被加载,越上级的父类,越先被加载。
(3)当使用类的静态成员时(静态属性,静态方法),类会被加载。
(4)注意,类的加载是在内存的方法区中。
2. 什么是代码块
(1)static 关键字修饰的代码块也叫静态代码块,它的作用就是对类进行初始化;它伴随着类的加载而执行,并且只会执行一次。
(2)没有关键字修饰的代码块也叫普通代码块,它相当于是构造器的补充机制;每当我们创建一个类的对象时,普通代码块就会被调用一次;如果只是直接使用类的静态成员,没有创建对象,它不会被调用;注意,普通代码块的调用与类的加载没有关系。
3. 创建一个对象时,在一个类中各属性和方法被虚拟机调用的顺序
(1)首先是调用静态代码块和进行静态属性初始化 (注意,调用静态代码块和静态属性初始化的优先级一样,若有多个静态代码块和静态属性初始化同时存在,则按他们定义的顺序使用),这一步是在类的加载时进行的。
(2)其次是正式在堆内存中创建一个对象空间,并调用普通代码块和进行普通属性的初始化 (注意,调用普通代码块和普通属性初始化的优先级一样,若有多个普通代码块和普通属性初始化同时存在,则按他们定义的顺序使用)。
(3)最后是调用类中的构造方法。
(4)补充:对于普通的静态方法,不会被虚拟机自动执行,啥时候被调用,啥时候执行。
- 代码举例说明:
public class CodeBlockDetail02 {
public static void main(String[] args) {
A a = new A();// 输出如下:
// (1) A 静态代码块01 (2) getN1被调用...
// (3) A 普通代码块01 (4) getN2被调用...
// (5) A() 构造器被调用
}
}
class A {
// 普通代码块
{
System.out.println("A 普通代码块01");
}
// 普通属性的初始化
private int n2 = getN2();
// 静态代码块
static {
System.out.println("A 静态代码块01");
}
// 静态属性的初始化
private static int n1 = getN1();
// 静态方法
public static int getN1() {
System.out.println("getN1被调用...");
return 100;
}
// 普通方法/非静态方法
public int getN2() {
System.out.println("getN2被调用...");
return 200;
}
// 无参构造器
public A() {
System.out.println("A() 构造器被调用");
}
}
4. 存在继承关系时,调用普通代码块和普通属性初始化的说明
(1)在构造器/构造方法的最前面其实隐含了 super()语句,还有调用普通代码块和普通属性的初始化的语句,最后才是构造器自己的语句。
(2)我们这里所说的普通属性的初始化,指的是属性在默认初始化后进行的,在构造器内的初始化(这一部分的概念是在面对对象基础部分学习的)。
- 代码举例说明:
public class CodeBlockDetail03 {
public static void main(String[] args) {
new BBB();
//(1) AAA的普通代码块 (2) AAA() 构造器被调用
//(3) BBB的普通代码块 (4) BBB() 构造器被调用
}
}
// 父类
class AAA {
// 普通代码块
{
System.out.println("AAA的普通代码块");
}
// 构造器
public AAA() {
//(1) super()
//(2) 调用本类的普通代码块和普通属性的初始化
System.out.println("AAA() 构造器被调用....");
}
}
// 子类
class BBB extends AAA {
// 普通代码块
{
System.out.println("BBB的普通代码块...");
}
public BBB() {
// (1)super()
// (2)调用本类的普通代码块和普通属性的初始化
System.out.println("BBB() 构造器被调用....");
}
}
5. 存在继承关系时,创建一个子类对象,各个方法和属性被虚拟机调用的顺序
- 首先是在类加载时,调用静态代码块和静态属性的初始化。
(1)父类的静态代码块和静态属性(优先级一样,按定义顺序执行)。
(2)子类的静态代码块和静态属性(优先级一样,按定义顺序执行)。- 其次是在堆内存中创建对象空间,调用普通代码块和普通属性的初始化,调用构造方法。
(3)在构造器中调用父类的普通代码块和普通属性的初始化(优先级一样,按定义顺序执行)。
(4)执行父类的构造器其余的方法。
(5)在构造器中调用子类的普通代码块和普通属性的初始化(优先级一样,按定义顺序执行)。
(6)执行子类的构造器其余的方法。
- 代码举例说明:
public class CodeBlockDetail04 {
public static void main(String[] args) {
// 说明
// (1) 进行类的加载
// 1.1 先加载 父类 A02 ;1.2 再加载子类 B02
// (2) 创建对象空间
// 2.1 从子类的构造器开始
new B02();// 创建对象
}
}
//父类
class A02 {
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 {
//
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)
}
}
五、单例设计模式
1. 什么是设计模式
- 设计模式是静态方法和静态属性的经典使用。
- 设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式就像是经典的棋谱,不同的棋局,我们可以使用不同的棋谱,免去了我们自己的摸索。
2. 什么是单例模式
- 单例(单个的示例)
- 所谓类的单例设计模式,就是采取一定的方法保证在整个的团软件系统中,对某个类的只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法。
- 单例模式有两种方式:(1)饿汉式;(2)懒汉式。
3. 饿汉式单例模式的实现
- 具体步骤:
(1)将类的构造器私有化,防止其他类直接创建该类对象。
(2)在类的内部创建一个 static 对象。
(3)该类向外提供一个静态的公共方法,返回 static 对象。
- 代码实现:
public class SingleTon01 {
public static void main(String[] args) {
//通过静态方法可以直接获取对象,
GirlFriend instance = GirlFriend.getInstance();
System.out.println(instance);
GirlFriend instance2 = GirlFriend.getInstance();
System.out.println(instance2);// instance和instance2 其实是同一个对象
System.out.println(instance == instance2);// true
}
}
// GirlFriend 类
// 要求只能有一个女朋友
class GirlFriend {
private String name;
//为了能够在静态方法中,返回 gf 对象,需要将其修饰为 static
//對象,通常是重量級的對象, 餓漢式可能造成創建了對象,但是沒有使用,造成资源的浪费
private static GirlFriend gf = new GirlFriend("小红红");
//如何保障我们只能创建一个 GirlFriend 对象
//步骤[单例模式-饿汉式]
//1. 将构造器私有化,防止其他类直接创建该类对象
//2. 在类的内部直接创建对象(该对象是static)
//3. 提供一个公共的static方法,返回 gf 对象
private GirlFriend(String name) {
System.out.println("構造器被調用...");
this.name = name;
}
public static GirlFriend getInstance() {
return gf;
}
@Override
public String toString() {
return "GirlFriend{" +
"name='" + name + '\'' +
'}';
}
}
- 思考:
(1)为什么要将公共的访问对象的方法 getInstance() 设置成静态方法,去掉static 可以吗?
- 不可以,我们想要在其他类中不创建该类的对象就可以调用该方法,因此只能将其设置为静态方法。
(2)为什么要类中创建 static 的对象,去掉static 修饰符可以吗?
- 不可以,在类中访问对象的方法 getInstance() 是静态方法,其只能返回静态的属性,因此只能将对象设置成 静态对象。
(3)饿汉式的弊端是什么?
- 弊端在于:在类加载时,该对象就会被自动创建了,无论有无使用该对象,它都存在,会造成资源的浪费。
4. 懒汉式单例模式的实现
- 具体步骤:
(1)将类的构造器私有化,防止其他类直接创建该类对象。
(2)在类的内部声明一个 static 对象,但没有真正创建对象空间。
(3)该类向外提供一个静态的公共方法,返回 static 对象。
(4)只有当用户第一次使用公共方法时,才创建对象空间;后面再次使用时,返回上次创建的对象。
- 代码实现:
public class SingleTon02 {
public static void main(String[] args) {
Cat instance = Cat.getInstance();
System.out.println(instance);
//再次調用 getInstance
Cat instance2 = Cat.getInstance();
System.out.println(instance2);
System.out.println(instance == instance2);// True
}
}
//希望在程序運行過程中,只能創建一個Cat對象
//Cat 类
class Cat {
private String name;
private static Cat cat; // 默認是null,并未真正创建对象空间
// 步驟
//(1)将类的构造器私有化,防止其他类直接创建该类对象。
//(2)在类的内部声明一个 static 对象,但没有真正创建对象空间。
//(3)该类向外提供一个静态的公共方法,返回 static 对象。
//(4)只有当用户第一次使用公共方法时,才创建对象空间;后面再次使用时,返回上次创建的对象。
// 從而保證了單例
private Cat(String name) {
System.out.println("構造器調用...");
this.name = name;
}
public static Cat getInstance() {
if(cat == null) {
//如果還沒有創建cat對象
cat = new Cat("小可愛");
}
return cat;
}
@Override
public String toString() {
return "Cat{" +
"name='" + name + '\'' +
'}';
}
}
5. 饿汉式和懒汉式的比较
- 如下图:
六、final 关键字
1. final 的基本概念
- 基本介绍:
- 代码演示:
public class Final01 {
public static void main(String[] args) {
E e = new E();
//e.TAX_RATE = 0.09; 错误,属性TAX_RATE 不能被修改
}
}
// (1)如果我们要求A类不能被其他类继承,可以使用final修饰 A类
final class A {
}
//class B extends A {} 错误,A类不能被继承;
class C {
//如果我们要求 hi 方法不能被子类重写,可以使用 final修饰 hi方法
public final void hi() {
}
}
class D extends C {
/* 错误,hi 方法不能被重写
@Override
public void hi() {
System.out.println("重写了C类的hi方法..");
}*/
}
//当不希望类的的某个属性的值被修改,可以用final 修饰
class E {
// TAX_RATE 属性也被称为常量;
public final double TAX_RATE = 0.08;
}
//当不希望某个局部变量被修改,可以使用final修饰
class F {
public void cry() {
// 这时,NUM 也称为 局部常量;
final double NUM = 0.01;
// NUM = 0.9; 错误,NUM 不能被修改
System.out.println("NUM=" + NUM);
}
}
2. final 使用注意事项和细节
- 注意事项和细节:
(1)final 修饰的属性又叫常量,一般用 XX_XX_XX 来命名。
(2)final 修饰的属性在定义时,必须赋初值,并且赋值后不能更改,赋值操作可以在以下位置(选择一个就行)。
- 在定义属性时:如 public final double TAX_RATE = 0.08;
- 在构造器中;
- 在代码块中(静态代码块或者普通代码块);
(3)若 final 修饰的属性同时被 static 修饰,则初始化的位置只能是以下位置(选择一个就行)。
- 在定义属性时:如 public final static double TAX_RATE = 0.08;
- 在静态代码块中(不能在普通代码块和构造器中赋值);
(4)final 类不能被继承,但是可以创建该类的对象。
(5)如果类没有被 final 修饰,但是含有 final 方法,则虽然该方法不能被子类重写,但是可以被子类继承。
- 代码演示:
public class FinalDetail01 {
public static void main(String[] args) {
CC cc = new CC();// 创建 final 修饰的CC类的对象
new EE().cal();// EE类可以使用 DD类中的 fanal 方法
}
}
class AA {
/*
(1)在定义属性时:如 public final double TAX_RATE = 0.08;
(2)在构造器中;
(3)在代码块中(静态代码块或者普通代码块);
*/
public final double TAX_RATE = 0.08;//1.定义时赋值
public final double TAX_RATE2 ;
public final double TAX_RATE3 ;
// 在构造器中赋值
public AA() {
TAX_RATE2 = 1.1;
}
// 在代码块中赋值
{
TAX_RATE3 = 8.8;
}
}
class BB {
/*
如果final修饰的属性是静态的,则初始化的位置只能是
(1)在定义属性时;
(2)在静态代码块中(不能在普通代码块和构造器中赋值);
*/
public static final double TAX_RATE = 99.9;//1.定义时赋值
public static final double TAX_RATE2 ;
// 在静态代码块中赋值
static {
TAX_RATE2 = 3.3;
}
}
//final类不能继承,但是可以实例化对象
final class CC {
}
//如果类不是final类,但是含有final方法,则该方法虽然不能重写,但是可以被继承
//即,仍然遵守继承的机制.
class DD {
public final void cal() {
System.out.println("cal()方法");
}
}
class EE extends DD {
}
(6)一般来说,如果一个类已经是 final 类了,就没有必要再将该类的方法修饰成 final 方法;因为final 类不能被继承,自然该类中的方法就不会被重写。
(7)final 类不能修饰构造器。
(8)final 和 static 往往一起搭配使用,效率更高;底层的编译器做了优化,使用两者结一起修饰的属性,在使用时不会导致类的加载,节约了资源。
(9)包装类(Integer、Double、Float…)和String 类都是 final 类,不能被继承。
- 代码演示:
public class FinalDetail02 {
public static void main(String[] args) {
// 不会输出静态代码块的内容,因为BBB 类没有被加载
System.out.println(BBB.num);
//包装类、String 都是 final 类,不能被继承;
}
}
// final 和 static 往往一起搭配使用,效率更高;
// 底层的编译器做了优化,使用两者结一起修饰的属性,在使用时不会导致类的加载,节约了资源。
class BBB {
// 在调用静态属性 num时,若是只有static 修饰,则会加载整个BBB 类,
// 但加上 final一起修饰后,则不会加载BBB 类
public final static int num = 10000;
static {
System.out.println("BBB 静态代码块被执行");
}
}
final class AAA{
// 一般来说,如果一个类已经是final类了,就没有必要再将方法修饰成final方法
// public final void cry() {}
}
七、抽象类
1. 抽象类和抽象方法的基本概念
(1)用 abstract 关键字来修饰一个类时,这个类就叫抽象类。基本语法:
- 访问修饰符 abstract 类名 {
}
(2)用 abstract 关键字来修饰一个方法时,这个方法就叫抽象方法。基本语法:
- 访问修饰符 abstract 返回类型 方法名 (形参列表);
- 当父类的一些方法不确定时,我们可以用 abstract 关键字来修饰该方法,这个方法就是抽象方法;抽象方法不需要具体实现,所谓没有具体实现就是该方法没有方法体;
- 而当一个类中存在抽象方法时,需要将该类声明为抽象类。一般来说,抽象类会被其子类继承,由其子类来实现抽象方法。
2. 抽象类使用事项和细节
(1)抽象类不能被直接实例化,即不能(new)创建抽象类的对象。(但是可以通过子类间接地实例化)
(2)抽象类不一定要包含抽象方法,也就是说,抽象类中可以没有 abstract 方法;但是存在 abstract 方法的类一定要声明为 抽象类(abstract)。
(3)abstract 关键字只能修饰类和方法。
- 代码举例:
public class AbstractDetail01 {
public static void main(String[] args) {
//抽象类,不能被实例化
//new A(); 错误
}
}
// 抽象类不一定要包含abstract方法。
// 也就是说,抽象类可以没有abstract方法
abstract class A {
public void hi() {
System.out.println("hi");
}
}
// 一旦类存在abstract方法,则这个类必须声明为abstract
abstract class B {
public abstract void hi();
}
//abstract 只能修饰类和方法,不能修饰属性和其它的
class C {
// public abstract int n1 = 1; 错误
}
(4)抽象类中可以包含任意成员,比如:非抽象方法、静态属性、构造器等等;抽象类本质上还是类,只能由 public 或者 默认 访问修饰符修饰。
(5)抽象方法中不能有方法体,即不能被实现,抽象以(;)号结束。
(6)如果一个类继承了抽象类,则它必须实现(重写)抽象类的所有抽象方法;除非它自己也声明为抽象类,这样就不用重写父类的抽象方法。
(7)抽象方法不能使用 private、final 和 static 关键字来修饰,因为这3个关键字都是与方法重写相违背的。
- private 修饰方法,将方法私有化,子类根本访问不了该方法,所以自然不能重写该方法。
- final 修饰方法,将方法直接设置为不能重写。
- static 修饰方法,将方法静态化,并与类绑定,所以子类是不能重写 static 方法的。
2. 抽象类的实践—模板设计模式
- 抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展和改造,但子类总体上会保留抽象类的行为方式。
- 模板设计模式能解决的问题:
- 案例要求:
有多个类,各自完成不同的任务job,要求统计各个类完成任务的时间。
- 代码实现:
// 抽象父类-模板设计模式
abstract public class Template {
public abstract void job(); //抽象方法
//实现方法,调用 job方法
public void calculateTime() {
//得到开始的时间
long start = System.currentTimeMillis();
job(); // 动态绑定机制
//得的结束的时间
long end = System.currentTimeMillis();
System.out.println("任务执行时间 " + (end - start));
}
}
// 子类
public class AA extends Template {
//实现Template的抽象方法job
@Override
public void job() {
long num = 0;
for (long i = 1; i <= 800000; i++) {
num += i;
}
}
}
// 子类
public class BB extends Template{
//这里也去,重写了Template的job方法
public void job() {
long num = 0;
for (long i = 1; i <= 80000; i++) {
num *= i;
}
}
}
// 测试类
public class TestTemplate {
public static void main(String[] args) {
AA aa = new AA();
aa.calculateTime();
BB bb = new BB();
bb.calculateTime();
}
}
八、接口
1. 接口的基本概念
- 接口:将一些抽象方法封装到一起,便形成了一个接口;当某个类需要使用这些方法时,便可以实现该接口,然后便可以使用这些方法了。
- 定义接口的基本语法:
interface 接口名 {
静态属性;
抽象方法;
}
- 类实现接口的基本语法:
class 类名 implements 接口名 {
重写接口的抽象方法;
}
- 总结:接口就是更加抽象的抽象类;接口实现了程序设计的多态和高内聚低耦合的设计思想。
- 特别说明:在jdk 8.0 之后,接口中可以有静态方法和默认方法(default 修饰)的存在,且只允许添加这两种。
*对接口的理解:
- 接口可以看做是父类的补充机制,可以对特定子类的功能进行扩展;接口也可以对某个需求设计进行规范。
- 举例来说,接口就像是一个包含了多个知识点(抽象方法)的技能,当某个人(某个子类)想要掌握这个技能的时候,就要把该技能的所有知识点都学会(重写抽象方法);同时,该技能可以对其知识点进行规范(比如知识点的名称、知识点的个数等等)。
2. 接口的注意事项和细节
(1)接口不能被实例化。
(2)接口中的所有的方法默认用 public 修饰符修饰(可以省略);接口中的抽象方法的abstract 关键字可以省略。
- void 方法名()== public abstract void 方法名()
(3)一个普通类实现接口,则必须重写接口中的所有抽象方法;抽象类实现一个接口,可以不用重写接口的抽象方法。
(4)一个类可以同时实现多个接口。
(5)接口中的属性,必须是 public static final 三者一起修饰的。
- int a = 1 == public static final int a = 1
(6)可以直接使用 接口名.属性 来访问接口中的属性。
(7)接口不可以继承类,但是可以继承多个其他的接口。
(8)接口的修饰符只能是 public 和 默认,和类的修饰符一样。
(9)若一个子类同时继承了父类和实现了一个接口,且父类和接口中存在同名的属性,则子类在调用该属性时,用 接口名.属性 表示调用接口,用 super.属性 表示调用父类。
3. 实现接口和继承关系的比较
(1)继承的价值主要在于:解决代码的复用性和可维护性。
(2)接口的价值主要在于:设计好各种规范(方法),让实现接口的类去具体实现这些规范(方法)。
(3)接口在一定程度上实现代码解耦:即接口规范性 + 动态绑定机制。
4. 接口的多态特性
- 接口的引用可以指向实现了接口的类的对象(并不是接口的实例化)。语法如下:
- 接口名 接口的引用 = new 类名();
4.1 接口的多态参数
- 可以将所有实现了接口的类的对象划分为同一组接口的引用(类比于继承的多态,可以将继承了同一个父类的所有子类的对象当做同一组父类的引用),然后将该引用作为方法的形参。
- 当传递给方法具体的类的对象作为实参时,根据动态绑定机制,可以调用正确的对象方法。
请比较接口的多态使用和继承的多态使用。
- 代码实现:
public class InterfacePolyParameter {
public static void main(String[] args) {
//接口的多态体现
//接口的引用 if01 可以指向 实现了IF接口的类的对象
IF if01 = new Monster();
if01 = new Car();
//继承体现的多态
//父类类型的变量 a 可以指向 继承AAA的子类的对象实例
AAA a = new BBB();
a = new CCC();
}
}
// 接口
interface IF {
}
class Monster implements IF{
}
class Car implements IF{
}
// 父类
class AAA {
}
class BBB extends AAA {
}
class CCC extends AAA {
}
4.2 接口的多态数组
- 可以将所有实现了接口的类的对象划分为同一组接口的引用(类比与类的多态,可以将继承了同一个父类的所有子类的对象当做同一组父类的引用)。
- 然后将该引用作为数组类型,创建一个数组,数组中的元素则可以是所有实现了接口的类的对象。
案例:给USB 数组中,存放 Phone 和 Camera 对象,Phone 类还有一个特有的方法 call(),请遍历USB 数组,如果是 Phone 对象,除了调用 USB 接口定义的方法外,还需要调用 Phone 特有的方法。
- 代码实现:
public class InterfacePolyArr {
public static void main(String[] args) {
//多态数组 -> 接口类型数组
Usb[] usbs = new Usb[2];
usbs[0] = new Phone_();
usbs[1] = new Camera_();
/*
给Usb数组中,存放 Phone 和 相机对象,Phone类还有一个特有的方法call(),
请遍历Usb数组,如果是Phone对象,除了调用Usb 接口定义的方法外,
还需要调用Phone 特有方法 call
*/
for(int i = 0; i < usbs.length; i++) {
usbs[i].work();// 动态绑定机制
// 需要进行类型的向下转型
if(usbs[i] instanceof Phone_) {
// 判断他的运行类型是 Phone_
((Phone_) usbs[i]).call();
}
}
}
}
interface Usb {
void work();
}
class Phone_ implements Usb {
public void call() {
System.out.println("手机可以打电话...");
}
@Override
public void work() {
System.out.println("手机工作中...");
}
}
class Camera_ implements Usb {
@Override
public void work() {
System.out.println("相机工作中...");
}
}
4.3 接口的多态传递
-
如果A 接口继承了B 接口,而C 类实现了A 类,那么实际上相当于C 类也实现了B类。
-
代码实现:
public class InterfacePolyPass {
public static void main(String[] args) {
//接口类型的变量可以指向,实现了该接口的类的对象实例
IG ig = new Teacher();
//如果IG 继承了 IH 接口,而Teacher 类实现了 IG接口
//那么,实际上就相当于 Teacher 类也实现了 IH接口.
//这就是所谓的 接口多态传递现象.
IH ih = new Teacher();
}
}
// IH 接口
interface IH {
void hi();
}
// IG 接口继承了IH接口
interface IG extends IH{
}
// 实现了IG 的类
class Teacher implements IG {
@Override
public void hi() {
}
}
九、内部类概述
1. 内部类的基本概念
- 内部类:一个类的内部又完整地嵌套了另一个类结构,则被嵌套的类成为内部类(inner class),嵌套其他类的类称为外部类(outer class)。
- 一个类中有五大成员:属性、方法、构造器、代码块、内部类。
- 内部类最大的特点就是可以直接访问外部类的私有属性,并且可以体现类与类之间的包含关系。
- 内部类是面向对象学习的重难点,底层源码中包含大量的内部类。
2. 基本语法
class Outer {
// 外部类
class Inner {
// 内部类
}
}
class Other {
// 外部其他类
}
3. 内部类的分类
(1)定义在外部类的成员位置上:
- 成员内部类(没有 static 修饰)
- 静态内部类(使用 static 修饰)
(2)定义在外部类的局部位置上(方法和代码块中):
- 局部内部类(有类名)
- 匿名内部类(无类名)重点!!!
十、局部内部类
- 局部内部类定义在外部类的局部位置(即方法和代码块中),并且具有类名。
1. 局部内部类的用法与细节
(1)局部内部类本质上还是类;但同时又相当一个局部变量,因此不能用任何访问修饰符和 static 来修饰局部内部类,可以使用 final 修饰。
(2)局部内部类的作用域只在定义它的方法或者代码块中。
(3)局部内部类可以直接访问外部类的所有成员,包括私有的(不需要创建外部类对象)。
(4)外部类的其他成员不能直接访问局部内部类的成员,只能通过在定义了局部内部类的方法中用局部内部类的对象调用其成员,然后再调用该方法,实现间接使用局部内部类的成员。
(5)外部其他类不能访问局部内部类。
- 把局部内部类看做是一个方法中的局部变量,便容易理解了。
(6)如果外部类中的成员和局部内部类中的成员重名时,内部类访问该成员默认遵循就近原则;在局部内部类中想访问外部类的同名成员,使用 外部类名.this.成员 访问(外部类名.this 就相当于外部类的对象)。
2. 细节代码演示
public class LocalInnerClass {
//
public static void main(String[] args) {
Outer02 outer02 = new Outer02();
outer02.m1();
System.out.println("outer02的hashcode=" + outer02);
}
}
class Outer02 {
// 外部类
private int n1 = 100;// 外部类的属性
// 外部类私有方法
private void m2() {
System.out.println("Outer02 m2()");
}
// 外部类普通方法
public void m1() {
// 局部内部类是定义在外部类的局部位置,方法和代码块中;
// 不能添加访问修饰符和 static ,但是可以使用 final 修饰;
// 作用域 : 仅仅在定义它的方法或代码块中;
class Inner02 {
// 局部内部类(本质仍然是一个类,同时也是方法的局部变量)
private int n1 = 800;// 局部内部类的私有属性(与外部类的同名)
public void f1() {
// 局部内部类可以直接访问外部类的所有成员,比如下面 外部类n1 和 m2();
// 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,使用 外部类名.this.成员 去访问;
// Outer02.this 本质就是外部类的对象, 即哪个对象调用了m1, Outer02.this就是哪个对象;
System.out.println("n1=" + n1 + " 外部类的n1=" + Outer02.this.n1);
// 打印Outer02.this 的哈希值,与main 方法中的对象 outer02比较,两者相同;
System.out.println("Outer02.this hashcode=" + Outer02.this);
m2(); // 直接访问外部类的私有方法;
}
} // 局部内部类结束
// 外部类在m1方法中,可以创建一个Inner02 对象,
// 这样就可以调用m1 方法,自动创建Inner02 对象,使用其成员了;
// 注意:不能在外部类的其他方法中创建m1 方法中的局部内部类对象;
// 因为局部内部类的作用域只在定义它的方法或者代码块中。
Inner02 inner02 = new Inner02();
inner02.f1();
}
}
十一、匿名内部类(重要!!!)
- 匿名内部类定义在外部类的局部位置(即方法和代码块中),并且不具有类名。
1. 匿名内部类的用法和细节
- 基本语法
// 第一种:实现接口的匿名内部类
接口名 接口的引用 = new 接口名() {
重写接口的抽象方法;
};
// 第二种:继承父类的匿名内部类(父类可以是抽象类或普通类)
父类类型 父类的引用 = new 父类(形参列表) {
重写父类的抽象方法/方法;
};
(1)匿名内部类本质上还是类,但同时它本身还是一个对象。
(2)匿名内部类就是实现了接口的一个类,或者是继承了父类的一个子类;匿名内部类只会被创建一次,之后便被销毁;其对象也只会被创建一次,但是该对象可以被一直使用。
- 代码解释:
// 实现接口的匿名内部类
public class AnonymousInnerClass {
public static void main(String[] args) {
Outer outer = new Outer();
outer.method();
}
}
class Outer {
//外部类
// 外部类的普通方法
public void method() {
// 实现接口的匿名内部类
IA tiger = new IA() {
@Override
public void cry() {
System.out.println("老虎叫唤...");
}
}; // 内部类创建完毕
// 查看匿名内部类的类名是否为 Outer$1
System.out.println("tiger的运行类型=" + tiger.getClass());
tiger.cry();// tiger对象 可以一直使用
tiger.cry();
tiger.cry();
}
}
// 接口
interface IA {
public void cry();
}
分析:
1.需求: 想实现IA接口,并创建对象
2.传统方式,是写一个实现接口的类,实现该接口,并创建该类对象
3.可要求是:该类只是使用一次,后面再不使用;
4. 可以使用匿名内部类来简化开发
5. tiger的编译类型是: IA接口
6. tiger的运行类型是: 就是匿名内部类 Outer$1 (jdk分配的类名,不能使用)
我们看底层分配 类名 Outer$1 ,如下:
class Outer$1 implements IA {
@Override
public void cry() {
System.out.println("老虎叫唤...");
}
}
7. jdk底层在创建匿名内部类 Outer$1,并立即就创建了 Outer$1 实例对象,
并且把对象地址返回给 tiger;
8. 匿名内部类 Outer$1 使用一次后,就不能再使用,但其对象 tiger可以一直使用。
// 继承父类的匿名内部类
public class AnonymousInnerClass {
public static void main(String[] args) {
Outer outer = new Outer();
outer.method();
}
}
class Outer {
//外部类
// 外部类的普通方法
public void method() {
// 继承父类的匿名内部类
Father father = new Father("jack"){
@Override
public void test() {
System.out.println("匿名内部类重写了test方法");
}
};
System.out.println("father对象的运行类型=" + father.getClass()); // 输出 Outer$2
father.test();
// 继承抽象类的匿名内部类
Animal animal = new Animal(){
@Override
void eat() {
System.out.println("小狗吃骨头...");
}
};
animal.eat();
}
}
// 父类
class Father {
// 构造器
public Father(String name) {
System.out.println("接收到name=" + name);
}
//方法
public void test() {
}
}
// 抽象类
abstract class Animal {
abstract void eat();
}
分析:
1. father编译类型 Father
2. father运行类型 Outer$2
3. 底层会创建匿名内部类,如下
class Outer$2 extends Father{
@Override
public void test() {
System.out.println("匿名内部类重写了test方法");
}
}
4. 同时也直接返回了 匿名内部类 Outer$2的对象
5. 注意("jack") 参数列表会传递给 构造器。
(3)匿名内部类既是一个类的定义,同时本身也是一个对象,又是一个局部变量。因此它可以不返回对象地址,通过自身直接调用其内部的成员;但这样该匿名内部类的对象便只能使用一次。
(4)匿名内部类的作用域只在定义它的方法或者代码块中;匿名内部类不能添加访问修饰符 和 static,可以用 final 修饰,因为它的地位就是一个局部变量。
(5)匿名内部类可以直接访问外部类的所有成员,包括私有的(不需要创建外部类对象)。
(6)外部类的其他成员不能直接访问匿名内部类的成员,只能通过在定义了匿名内部类的方法中用匿名内部类的对象调用其成员,然后再调用该方法,实现间接使用匿名内部类的成员。
(7)外部其他类不能访问匿名内部类及其成员。
(8)如果外部类中和成员和匿名内部类中的成员重名时,内部类访问该成员默认遵循就近原则;在匿名内部类中想访问外部类的同名成员,使用 外部类名.this.成员 访问。
- 代码解释:
public class AnonymousInnerClassDetail {
public static void main(String[] args) {
Outer05 outer05 = new Outer05();
outer05.f1();
System.out.println("main outer05 hashcode=" + outer05);
}
}
class Outer05 {
private int n1 = 99;// 外部类的属性
public void f1() {
// 创建一个基于类的匿名内部类;
// 不能添加访问修饰符,因为它的地位就是一个局部变量;
// 作用域 : 仅仅在定义它的方法或代码块中
Person p = new Person() {
private int n1 = 88;// 匿名内部类的属性,与外部类的同名
@Override
public void hi() {
// 匿名内部类可以直接访问外部类的所有成员,包含私有的;
// 如果外部类和匿名内部类的成员重名时,匿名内部类访问的话,默认遵循就近原则;
// 如果想访问外部类的成员,则可以使用(外部类名.this.成员)去访问;
System.out.println("匿名内部类重写了 hi方法 n1=" + n1 +
" 外部内的n1=" + Outer05.this.n1 );
// Outer05.this 就是调用 f1的 对象;
System.out.println("Outer05.this hashcode=" + Outer05.this);
}
};// 匿名内部类定义结束
p.hi();// 动态绑定, 运行类型是 Outer05$1
// 匿名内部类也可以直接调用其成员, 匿名内部类本身也是一个对象
new Person() {
@Override
public void hi() {
System.out.println("匿名内部类重写了 hi方法,哈哈...");
}
@Override
public void ok(String str) {
super.ok(str);
}
}.ok("jack");
}
}
// 父类
class Person {
public void hi() {
System.out.println("Person hi()");
}
public void ok(String str) {
System.out.println("Person ok() " + str);
}
}
2. 匿名内部类的最佳实践
- 匿名内部类可以当做实参直接传递给方法,简洁高效。
案例如下:
public class InnerClassExercise01 {
public static void main(String[] args) {
// 传统方法:传入一个实现接口的对象,浪费资源;
f1(new Picture());
// 新方法:把匿名内部类当做实参直接传递,简洁高效。
f1(new IL() {
@Override
public void show() {
System.out.println("这是一副名画~~...");
}
});
}
// 静态方法,形参是接口类型
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...");
}
}
十二、成员内部类
- 成员内部类是定义在外部类成员位置的类,并且没有 static 关键字修饰。
成员内部类的用法与细节
(1)成员内部类实质上就是一个类;同时它也是一个成员变量,因此它可以用任意的访问修饰符来修饰(public、protected、默认、private),也可以用 final 来修饰,但不用 static 修饰(用 static 修饰的成员内部类叫做静态内部类)。
(2)成员内部类的作用域是整个类体;它可以直接访问外部类的所有成员,包含私有的、静态的(不需要创建外部类对象);注意:成员内部类中不能定义静态成员。
(3)外部类的其他成员想访问成员内部类中的非静态成员,需要先创建成员内部类的对象,再通过该对象来访问(成员内部类中没有静态成员)。
(4)外部其他类的成员访问成员内部类的成员(前提是满足访问权限),有两种方式,如下:
// 首先需要先创建一个外部类的对象;
Outer08 outer08 = new Outer08();
// 第一种方式: 直接在外部其他类中创建成员内部类对象;
// 这就是一个语法,不要特别的纠结
Outer08.Inner08 inner08 = outer08.new Inner08();
// outer08.new Inner08(); 相当于把 new Inner08() 作为整体当做是 outer08 成员
inner08.say(); // 这时便可以使用成员内部类的成员了
// 第二方式: 在外部类中,编写一个方法,可以返回 Inner08 对象;
// public Inner08 getInner08Instance(){ return new Inner08(); }
Outer08.Inner08 inner08Instance = outer08.getInner08Instance();
inner08Instance.say();// 这时便可以使用成员内部类的成员了
(5)如果外部类中的成员和成员内部类中的成员重名时,内部类访问该成员默认遵循就近原则;在成员内部类中想访问外部类的同名成员,使用 外部类名.this.成员 访问。
十三、静态内部类的用法与细节
- 静态内部类是定义在外部类成员位置的类,并且使用 static 关键字修饰。
静态内部类的用法与细节
(1)静态内部类实质上就是一个类;同时它也是一个成员变量,因此它可以用任意的访问修饰符来修饰(public、protected、默认、private),也可以用 final 来修饰,而必须使用 static 修饰。
(2)静态内部类的作用域是整个类体;它可以直接访问外部类的所有静态成员,包含私有的(不需要创建外部类对象);注意:静态内部类中可以定义非静态成员和静态成员(自己尝试)。
(3)外部类的其他成员想访问静态内部类中的非静态成员,需要先创建静态内部类的对象,再通过该对象来访问;外部类的其他成员想访问静态内部类中的静态成员,则只需要使用静态内部类名.成员 便可直接访问。
(4)外部其他类的成员访问成员内部类的成员(前提是满足访问权限),有两种方式,如下:
// 首先需要先创建一个外部类的对象;
Outer outer = new Outer();
// 第一种方式: 直接在外部其他类中创建静态内部类对象;
// 这就是一个语法,不要特别的纠结
Outer.Inner inner = new Outer.Inner();
// 因为静态内部类,是可以通过类名直接访问(前提是满足访问权限)
inner.say(); // 这时便可以使用静态内部类的成员了
// 第二方式: 在外部类中,编写一个方法,可以返回 Inner 对象;
// public Inner getInnerInstance(){ return new Inner(); }
Outer.Inner innerInstance = outer.getInnerInstance();
innerInstance.say();// 这时便可以使用静态内部类的成员了
(5)如果外部类中的成员和静态内部类中的成员重名时,内部类访问该成员默认遵循就近原则;在静态内部类中想访问外部类的同名成员,使用 外部类名.成员 访问(静态内部类中不能使用 this 关键字)。
总结
- 本文详细总结讲解了面向对象编程高级部分的知识,并深入解释了每个知识点的注意事项和细节。面向对象编程高级部分的内容时基础和中级的结合提示,同时又加入了新的概念,是Java基础的重难点,也是一道分水岭,理解并掌握这一部分的知识,需要有较好的面向对象的基础,如果文中出现的所有未仔细讲解的知识都在博主的面向对象编程基础和中级篇讲解过,小伙伴们有兴趣可以去看看。小白博主已经尽力整理了,希望小伙伴们看后能有所收获!
- 最后,如果本文有什么错漏的地方,欢迎大家批评指正!一起加油!!我们下一篇博文见吧!