面向对象编程(高级)
1. 类变量和类方法(static)
1.1 类变量基本介绍
-
类变量也叫静态变量/静态属性,是该类的所有对象共享的变量,任何一个该类的对象去访问它时,取到的都是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量。
-
不管 static 变量在哪里,共识:(1) static 变量是同一个类所有对象共享;(2) static 类变量,在类加载的时候就生成了,所以即使没有创建对象实例也可以访问(类名.类变量名)
-
定义类变量语法:
访问修饰符 static 数据类型 变量名;(推荐)
static 访问修饰符 数据类型 变量名;
-
访问类变量:
类名.类变量名(推荐) 或者 对象名.类变量名
-
类变量的访问也要遵守访问权限规则。
1.2 类变量的使用细节
-
当我们需要让某个类的所有对象都共享一个变量时,就可以考虑使用类变量(静态变量)。比如:定义学生类,每个学生的住宿费是统一的,可以定义为类变量。
-
类变量与实例变量(普通属性)区别:类变量是该类的所有对象共享的,而实例变量是每个对象独享的。
-
加上 static 称为类变量或静态变量,否则称为 实例变量/普通变量/非静态变量
-
类变量可以通过 类名.类变量名 或者 对象名.类变量名 来访问,但java设计者推荐我们使用 类名.类变量名 方式访问。【前提是满足访问修饰符的访问权限和范围】
-
实例变量不能通过类名.类变量名方式访问。
-
类变量是在类加载时就初始化了,也就是说,即使你没有创建对象,只要类加载了,就可以使用类变量了(通过类名.类变量名)。
public static void main(String[] args) { System.out.println(A.n1); //错误 System.out.println(A.n2); //正确 } class A{ public int n1 = 100; // 实例变量/普通变量/非静态变量 public static int n2 = 200; //类变量/静态变量 }
-
类变量的生命周期是随类的加载开始,随着类消亡而销毁。[][案例演示]
1.3 类方法基本介绍
-
类方法也叫静态方法。定义类方法的语句:
访问修饰符 static 数据返回类型 方法名(){}
static 访问修饰符 数据返回类型 方法名(){}
-
类方法的调用:
类名.类方法名 or 对象名.类方法名
注意:类方法的调用也要满足访问权限
-
当方法中不涉及到任何和对象相关的成员,则可以将方法设计成静态方法,提高开发效率。比如:将一个冒泡排序方法设置成静态方法,以后需要排序时直接调用 类名.冒泡排序方法名,就不用了重新实例化一个对象
1.4 类方法的使用细节
特别说明:类方法 = 静态方法; 类变量 = 静态变量
-
类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区:类方法中无 this 的参数;普通方法中隐含着 this 的参数
-
类方法可以通过类名调用,也可以通过对象名调用;普通方法只能通过对象名调用。比如:
//类 A 有静态方法 say(),非静态方法 talk() A.say(); //正确 new A().say(); //正确 new A().talk(); //正确
-
==类方法中不允许使用和对象有关的关键字,比如 this 和 super 。==普通方法(成员方法)可以。
-
静态方法,只能访问静态的成员;普通方法,既可以访问静态成员,又可以访问非静态成员(必须遵守访问权限)
1.5 课堂练习
对题2:main 中的第一个 println 语句中没有实例化对象,没有调用构造器。第二句 Person p1 = new Person(); 实例化了一个对象,调用构造器,total++ 被执行,故 total 变成 1。
对题 3:setTotalPerson() 是静态方法,不能有关键字 this 或者 super,故 this.total = total;语句错误。main 中的语句 new Person();相当于实例化了一个对象,会执行构造器,但是没有返回给对象的引用,所以 total++ 被执行。
2. main
2.1 main 语法说明
解释main方法的形式:public static void main(String[] args){…}
-
main 方法是 java 虚拟机调用
-
public:java 虚拟机需要调用类的 main() 方法,所以该方法的访问权限必须是 public
-
static:java 虚拟机在执行 main() 方法时不必创建对象,所以该方法必须是 static。
-
String[] args:该方法接收String类型的数组参数,从命令行输入,该数组中保存执行 java 命令时传递给所运行的类的参数,演示接收参数如下:
- java 执行的程序 参数1 参数2 参数3
2.2 main 特别说明
-
在 main() 方法中,我们可以直接调用 main 方法所在类的静态方法或静态属性。
-
但是,不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静
态成员
- 如何在 IDEA 中给 args 传值。 (2.1 中的 4 是在 DOS 命令中传递的值)
设置好后再次执行,结果如下:
3. 代码块
3.1 代码块基本介绍
代码化块又称为初始化块,属于类中的成员【即是类的一部分】,类似于方法,将逻辑语句封装在方法体中,通过 {} 包围起来。
但和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显式调用,而是加载类时,或创建对象时隐式调用。
基本语法:
[修饰符]{
代码
};
1、修饰符可选,要写的话,也只能写 static
2、代码块分为两类,使用 static 修饰的叫静态代码块,没有 static 修饰的,叫普通代码块/非静态代码块。
3、逻辑语句可以为任何逻辑语句(输入、输出、方法调用、循环、判断等)
4、;号可以写上,也可以省略。
3.2 代码块用法
public class CodeBlock {
public static void main(String[] args) {
Movie movie = new Movie("你好,李焕英");
System.out.println("=======================");
Movie movie1 = new Movie("唐探3", 100, "陈思诚");
}
}
class Movie{
private String name;
private double price;
private String director;
//代码块
{
System.out.println("电影开始了......");
System.out.println("小朋友坐端正......");
};
//2 个构造器-》重载
// (1) 下面的2个构造器都有相同的语句
// (2) 这样代码看起来比较冗余
// (3) 这时我们可以把相同的语句,提出来,放入到一个代码块中(如上),即可
// (4) *******这样当我们不管调用哪个构造器,创建对象,都会先调用代码块的内容******
// (5) *****代码块调用的顺序优先于构造器****
public Movie(String name) {
//System.out.println("电影开始了......");
//System.out.println("小朋友坐端正......");
System.out.println("Movie(String name)被调用......");
this.name = name;
}
public Movie(String name, double price, String director) {
//System.out.println("电影开始了......");
//System.out.println("小朋友坐端正......");
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.3 代码块的使用细节
-
static 代码块也叫静态代码块,作用就是对类进行初始化,而且它**随着类的加载而执行,并且只会执行一次**。如果是普通代码块,每创建一个对象,就执行。
-
类什么时候被加载:
①、创建对象实例时,即 new 时
//main方法中 AA aa = new AA(); //初始化时,先加载类的信息,此时会加载静态代码块的信息,只加载一次 class AA { //静态代码块 static { System.out.println("AA 的静态代码 1 被执行..."); } } //输出: /* AA 的静态代码 1 被执行... */
②、创建子类对象实例,父类也会被加载
//main方法中 AA aa = new AA(); //初始化时,先加载父类的信息即BB,再加载子类的信息。 class BB { //静态代码块 static { System.out.println("BB 的静态代码 1 被执行..."); } } class AA extends BB { //静态代码块 static { System.out.println("AA 的静态代码 1 被执行..."); } } //输出: /* BB 的静态代码 1 被执行... AA 的静态代码 1 被执行... */
③、使用类的静态成员时(静态属性,静态方法)
//main 方法中 System.out.println(Cat.age); //因为用到了 Cat.age 所以会加载 Cat 类 class Cat{ public static int age = 999; //访问权限为 public 其他类才能直接访问 //静态代码块 static { System.out.println("Cat的静态代码块被执行..."); }; } //输出 /* Cat的静态代码块被执行... 999 */
-
普通的代码块,在创建对象实例(new)时,会被隐式的调用。被创建一次,就会调用一次。(参见 3.2 中,两个对象都调用了普通代码块)
如果只是使用类的静态成员时,普通代码块并不会执行。
public class CodeBlock { public static void main(String[] args) { System.out.println(DD.n1); } } class DD { public static int n1 = 8888;//静态属性 // 静态代码块 static { System.out.println("DD 的静态代码 1 被执行...");// } //普通代码块, 在 new 对象时,被调用,而且是每创建一个对象,就调用一次 //可以这样简单的理解: 普通代码块是构造器的补充 { System.out.println("DD 的普通代码块..."); } } //输出: //DD 的静态代码 1 被执行... //8888
-
创建一个对象时,在一个类调用顺序是:
①、先调用静态代码块和静态属性初始化。(注意:静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态属性初始化,则按照定义的顺序调用)
②、再调用普通代码块和普通属性的初始化。(注意:普通代码块和普通属性初始化调用的优先级一样,如果有多个,则按照定义的顺序调用)
③、最后调用构造器。
总结:为什么先 静态——普通——构造器,因为静态是与类加载有关的,所以最先执行;普通是与对象初始化开辟空间有关的,所以实例化对象时第二执行;构造器是根据赋值 new A(参数) 来给对象的数据空间赋值的,所以最后执行。(注意:看下一条——第5条,这里分析有问题,普通也是包含在构造器中,只是在构造器前面执行的)
//main方法中 A a = new A();//按照 静态——普通——构造器 的过程进行 class A { private int n2 = getN2(); private static int n1 = getN1();//这里的getN1()必须是静态方法 public A() { System.out.println("A的构造器被调用...(顺序 5)"); } { System.out.println("A的普通代码块被调用...(顺序 4)"); } public int getN2() { System.out.println("getN2()被调用(普通属性n2)...(顺序 3)"); return 99; } static { System.out.println("A的静态代码块被调用...(顺序 2 )"); } public static int getN1() { System.out.println("getN1()被调用(静态变量n1)...(顺序 1)"); return 100; } } //输出是按照后面顺序 1 2 3 4 5 输出的
-
构造器 的最前面其实隐含了 super() 与 调用普通代码块执行和普通属性初始化。(注意:此时已经完成了静态相关的部分,因为构造器最后执行嘛,因此静态相关代码是优于 构造器和普通代码块执行的):
class A{ public A(){ //构造器 //1.super(); //2.调用本类的普通代码块,初始化本类的普通属性(优先级一样) System.out.println("OK"); //实际上在这一句上面还有两句隐藏的:先是super(); 再是调用普通代码块和普通属性初始化 //故super比普通代码块先执行(super和this应该在构造器第一行,还是有道理) } }
-
当创建一个子类时,其代码块、属性和构造器的调用顺序为:父静——子静——父普——父构——子普——子构(十分重要!!!)
其中父静表示父类的静态代码块和静态属性(优先级一样,按定义顺序执行);父普表示父类的普通代码块和普通属性(优先级一样,按定义顺序执行);父构表示父类的构造方法。子静、子普、子构同理。
①、先明白一个通识:Cat cat = new Cat(); 即创建一个对象实例时,先加载类信息(先父类,再子类),再创建对象(开辟空间),加载类信息与静态相关,创建对象与构造器有关(构造器中第一句为父类的 super,第二句为普通代码块执行和普通属性初始化,优先级一样)
②、上面顺序:父静、子静是跟类加载相关的,父普、父构、子普、子构是跟对象创建相关的
-
静态代码块只能直接调用静态成员(静态属性和静态方法),普通代码块可以调用任意成员
3.4 课堂练习
- 下面代码输出什么:
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);
}
}
/*
输出:
in static block!
100
100
*/
考点:①、类名.静态属性名 访问静态属性;②、加载类信息时,静态代码块和静态属性初始化都会被执行,并且只执行一次
-
下面代码输出什么:
class Sample { Sample(String s) { //有参构造器 System.out.println(s); } Sample() { //默认构造器 System.out.println("Sample 默认构造函数被调用"); } } class Test { Sample sam1 = new Sample("sam1 成员初始化"); //普通属性 static Sample sam = new Sample("静态成员 sam 初始化 "); //静态属性 static { //静态代码块 System.out.println("static 块执行"); if (sam == null) System.out.println("sam is null"); } Test() { System.out.println("Test 默认构造函数被调用"); } } //主方法main Test a = new Test(); /*1. 静态成员 sam 初始化 2. static 块执行 3. sam1 成员初始化 4. Test 默认构造函数被调用*/
分析:创建一个对象时,先加载类信息(执行静态的东西),再执行构造器(先是普通属性的初始化 和 普通代码块的调用(此时无父类),然后是构造器本身)
4. 单例设计模式
4.1 设计模式
- 静态方法和属性的经典使用
- 设计模式是在大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式。设计模式就像是经典的棋谱,不同的棋局,我们用不同的棋谱,免去我们自己再思考和摸索。
4.2 单例设计模式
- 所谓类的单例设计模式,就是采取一定的方法保证在整个的软件系统中,对某个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法
- 单例模式有两种方式:①饿汉式;②懒汉式
4.3 应用
演示饿汉式和懒汉式单例模式的实现。
- 构造器私有化,这是为了防止直接 new
- 类的内部直接创建对象(该对象是 static)
- 在类中向外暴露一个 static 的公共方法——getlnstance() 用来返回一个对象实例。
- main 中使用 类名.getlnstance() 来获得一个对象实例
饿汉式:类加载的时候对象已经被创建好了,即没有使用的时候已经创建了对象实例。饿汉式可能造成创建了对象但是没有使用。代码如下:
//main中
public static void main(String[] args) {
GirlFriend gf = GirlFriend.getInstance();//通过调用公开函数来得到一个对象实例,如果再次调用getInstance()还是原对象,从而保证单例
System.out.println(gf);
System.out.println(GirlFriend.n1); //注释掉上上面那一句,还是会输出“构造器被调用”,因为此时会加载类
//而加载类的时候就会创建静态的对象(因为有new)
}
class GirlFriend{
private String name;
public static int n1 = 100; //为什么是static因为main函数是static,所以要想直接调用n1,n1必须为static
private static GirlFriend instance = new GirlFriend("二狗");//static 和 new是为了在加载类的时候就把该对象创建好(饿汉式精髓)
//返回该类的对象,用于在main中得到一个实例化的对象,因为要用到静态属性instance 所以次函数也必须为静态的
public static GirlFriend getInstance(){
return instance;
}
private GirlFriend(String name) { //构造器私有化,只能类内部创建对象
System.out.println("构造器被调用");
this.name = name;
}
@Override //重写便于输出
public String toString() {
return "GirlFriend{" +
"name='" + name + '\'' +
'}';
}
}
懒汉式:只有当用户使用 getInstance() 这个方法的时候才创建对象实例,后面如果再次调用 getInstance() ,只会返回上一次创建的对象。代码如下:
public static void main(String[] args) {
GirlFriend gf = GirlFriend.getInstance();//执行这一句的时候静态的instance为空才会执行构造器创建对象,
//如果不执行getInstance()就不会创建,再次调用也只会返回第一次调用时创建的那个对象实例
System.out.println(gf);
System.out.println(GirlFriend.n1);//注释掉上面两行,不会输出“构造器被调用”,因为定义静态属性的时候没有用new
}
}
class GirlFriend{
private String name;
public static int n1 = 100;
private static GirlFriend instance;//这里没有new所以加载类的时候不会直接创建对象实例
public static GirlFriend getInstance(){//调用函数时,先判断instance属性有没有被创建,如果没有被创建才重新创建
if(instance == null){
instance = new GirlFriend("二狗");
}
return instance;
}
private GirlFriend(String name) {
System.out.println("构造器被调用");
this.name = name;
}
@Override
public String toString() {
return "GirlFriend{" +
"name='" + name + '\'' +
'}';
}
}
4.4 饿汉式 VS 懒汉式
-
二者最主要的区别在于创建对象的时机不同:饿汉式是在类加载就创建了对象实例,而懒汉式是在使用时才创建。
-
饿汉式不存在线程安全问题,懒汉式存在线程安全问题。(后面学习线程后,会完善一把)
-
饿汉式存在浪费资源的可能。因为如果程序员一个对象实例都没有使用,那么饿汉式创建的对象就浪费了,懒汉式是使用时才创建,就不存在这个问题。
-
在我们 javaSE 标准类中,java.lang.Runtime 就是经典的单例模式。源码如下,可以看出是饿汉式:
5. final 关键字
5.1 final 基本介绍
final 可以修饰类、属性、方法和局部变量。
在某些情况下,程序员可能有以下需求,就会使用到 final:
- 当不希望类被继承时,可以用 final 修饰。如:final class A{…}
- 当不希望父类的某个方法被子类覆盖/重写(override)时,可以用 final 关键字修饰。如:访问修饰符 final 返回类型 方法名(){…}
- 当不希望类的的某个属性的值被修改,可以用final修饰。【案例演示:public final double TAX_ RATE=0.08】
- 当不希望某个局部变量被修改,可以使用final修饰【案例演示: final double TAX RATE=0.08 】
5.2 final 使用细节
-
final 修饰的属性又叫常量,一般用 XX_XX_XX(大写) 来命名
-
final 修饰的属性在定义时必须赋初值,并且以后不能再修改,赋值可以在如下位置之一【选择一个位置赋初值即可】:
①、定义时:如 public final double TAX_RATE = 0.08;
②、在构造器中
③、在代码块中。
class AA{
public final int A_NUM = 100; //1. 定义时直接赋值
public final int B_NUM; //2.构造器中赋值
public AA() {
B_NUM = 200;
}
public final int C_NUM; //3.代码块中赋值
{
C_NUM = 300;
}
}
//即实例化对象时(因为上面的3种赋值都是在实例化对象完成之前),即满足实例化后该对象的常量属性必须是已知的(值确定的)
-
如果 final 修饰的属性是静态的,则初始化的位置只能是
①、定义时
②、在静态代码块,但是不能在构造器中赋值。
class AA{ public static final int A_NUM = 100; //1.定义时赋值 public static final int B_NUM; //2.静态代码块中赋值 static { B_NUM = 200; } }
-
final类不能继承,但是可以实例化对象。
-
如果类不是 final 类,但是含有 final 方法,则该方法虽然不能重写,但是可以被继承。
-
一般来说,如果一个类已经是final类了,就没有必要再将方法修饰成 final 方法(因此他已经没法被继承了,方法当然也不能被重写)。
-
final 不能修饰构造方法(即构造器)
-
final 和 static 往往搭配使用,效率更高,不会导致类加载(底层编译器做了优化处理),如下:
public static void main(String[] args) { System.out.println(AA.NUM); //只会输出 99 ,不会输出“静态代码块被加载...” } class AA{ public final static int NUM = 99; //如果删除了final,则会输出:“静态代码块被加载...” 和 99 static { System.out.println("静态代码块被加载..."); } }
-
包装类(Integer,Double,Float,Boolean 等都是 final ),String 也是 final 类(不允许他们被继承)。
5.3 课堂练习
public int addOne(final int x) { //下面的代码是否有误,为什么? 1min
++x; //错误,原因是不能修改 final x 的值
return x + 1; //这里是可以. 因为没有改变X的值
}
}
6. 抽象类(abstract)
6.1 抽象类基本介绍
当父类的一些方法不能确定时,可以用 abstract 关键字来修饰该方法,这个方法就是抽象方法,用 abstract 来修饰该类就是抽象类。
-
用 abstract 关键字来修饰一个类时,这个类就叫抽象类,语法如下:
访问修饰符 abstract 类名 {…};
-
用 abstract 关键字来修饰一个方法时,这个方法就是抽象方法,语法如下:
访问修饰符 abstract 返回类型 方法名(参数列表);(注意:没有方法体)
-
抽象类的价值更多作用是在于设计,是设计者设计好后,让子类继承并实现抽象类
-
抽象类,是考官比较爱问的知识点,在框架和设计模式使用较多
6.2 抽象类使用细节
-
抽象类不能被实例化,如 new A();错误的
-
抽象类不一定要包含 abstract 方法。
-
一旦类包含了 abstract 方法,则这个类必须声明为 abstract 类
-
abstract 只能修饰类和方法,不能修饰属性和其他的
-
抽象类可以有任意成员(抽象类的本质还是类),比如非抽象方法、构造器、静态属性等等
-
抽象方法不能有主体,即 {…}
-
如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它自己也声明为 abstract 类
-
抽象方法不能使用 private、final 和 static 来修饰,因为这些关键字都是和重写相违背的,如下:
abstract class H{ private abstract void hi();//错误的 public final abstract void hi1();//错误的,因为final修饰了方法后,该方法不允许被重写。而abstract本身就是要让子类去重写的 public static abstract void hi2();//错误的 }
6.3 课堂练习
public class Abstract01 {
public static void main(String[] args) {
Manager Em1 = new Manager("二狗", "929751708", 50000, 80000);
Em1.work();
}
}
class Manager extends Employee{
private double bonus;
public Manager(String name, String id, double salary, double bonus) {//构造器
super(name, id, salary);
this.bonus = bonus;
}
@Override
public void work() {
System.out.println("经理:" + this.getName() + " 工作中...");
}
}
abstract class Employee{
private String name;
private String id;
private double salary;
public Employee(String name, String id, double salary) {//构造器
this.name = name;
this.id = id;
this.salary = salary;
}
//省略get和set方法
public abstract void work(); //抽象方法
}
7. 模板设计模式
7.1 说明
抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。
- 当功能内部一部分实现是确定,一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。
- 编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给其子类实现,就是一种模板模式.
7.2 例子
需求:
- 有多个类,完成不同的任务 job
- 要求统计得到各自完成任务的时间
- 请编程实现
想法:
-
先用最容易想到的方法(每个类中用 System.currentTimeMillis() ),如下:
class AA{ public void job(){ long start = System.currentTimeMillis(); long num = 0; for (int i = 0; i < 100000; i++) { num += i; } long end = System.currentTimeMillis(); System.out.println("AA 执行的时间为:" + (end - start)); } } //每一个类中有三句都是重复的: /* long start = System.currentTimeMillis(); ... long end = System.currentTimeMillis(); System.out.println("AA 执行的时间为:" + (end - start)); 由此分析我们可以在每一个类中创建一个 calculateTime()的函数,把每一个类中job()方法放到中间,即 public void calculateTime(){ long start = System.currentTimeMillis(); job(); long end = System.currentTimeMillis(); System.out.println("AA 执行的时间为:" + (end - start)); } 虽然简化了一点但是还是有点复杂,因此考虑到抽象类,如2 */
-
分析问题,提出使用模板设计模式
public class Abstract02 {
public static void main(String[] args) {
AA aa = new AA();
aa.calculateTime();//这里也是动态绑定机制,即OOP机制
BB bb = new BB();
bb.calculateTime();
}
}
abstract class Template {
public abstract void job();//抽象方法
public void calculateTime(){
long start = System.currentTimeMillis();
job(); //动态绑定机制,哪个对象调用就跟哪个对象绑定 ,执行到job方法这里是跟对象绑定的,所以回去对象中找该方法
//main中对象aa调用,跟aa绑定,bb调用,跟bb绑定
long end = System.currentTimeMillis();
System.out.println("任务执行的时间:" + (end - start));
}
}
class AA extends Template{
@Override
public void job() {
long num = 0;
for (int i = 0; i < 100000; i++) {
num += i;
}
}
}
class BB extends Template{
@Override
public void job() {
long num = 0;
for (int i = 0; i < 800000; i++) {
num += i;
}
}
}
8. 接口(Interface)
8.1 接口的基本介绍
接口就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,再根据具体情况把这些方法写出来。
语法:
interface 接口名{
//接口的属性
//接口的方法(1.抽象方法;2.默认实现方法;3.静态方法)
}
class 类名 implements 接口{
//类的属性;
//类的方法;
//必须实现的接口的抽象方法;
}
-
在 JDK 7.0 前接口里面的所有方法都没有方法体,即都是抽象方法。
-
在 JDK 8.0 后接口可以有静态方法,默认方法,也就是说接口中可以有方法的具体实现。
//接口 public interface Interface01 { public int n1 = 10; //接口的属性 public void hi(); //接口的抽象方法(此处可以省略abstract关键字) public static void ok(){//接口的方法声明为静态可以写实体 System.out.println("ok()......"); } default public void cry(){//接口的方法声明为default(默认)可以写实体 System.out.println("cry()......"); } public void say(){//此句错误 ,没有static或者default修饰,默认为abstract,所以不能有方法体 System.out.println("123....."); } }
//实现上面接口的类 class A implements Interface01{//如果一个类implements实现接口,需要将该接口的所有抽象方法都实现 @Override public void hi() { System.out.println("hi()......"); } }
8.2 接口的使用细节
-
接口不能被实例化。抽象类和接口一样 ,都不能被实例化
-
接口中的所有方法是 public 方法;接口中抽象方法可以不用 abstract 修饰
interface IA{ void say(); //这句话写完整就是:public abstract void say(); }
-
一个普通类实现接口,就必须将该接口的所有方法(除static修饰的方法外)都实现(可以使用 Alt + enter 或者 Alt + Insert 来实现)
-
抽象类实现接口,可以不用实现接口中的抽象方法
-
一个类同时可以实现多个接口
interface IB{} interface IA{} class AA implements IB,IC{ //实现接口的方法 }
-
接口中的属性,只能是 final 的,而且是public static final修饰的(但是 public static final 可以省略)
public class Test { public static void main(String[] args) { System.out.println(IA.a);//直接通过 类名.属性名 访问,证明了静态static IA.a = 3;//这句话会报错,说明不能修改 IA.a 的值,证明了final } } interface IA{ int a = 1; //实际上是:public static final int a = 1; }
-
接口中属性的访问形式:接口名.属性名
-
接口不能继承类,但是接口可以继承接口(接口与接口的关系可以是继承 extends,接口与类的关系只能是实现 implements)。如果接口 A 继承了接口B ,类 C 实现了接口 A,那么 C 也实现了 父类接口 B。
-
接口的修饰符只能是 public 和 默认,这一点跟类的修饰符是一样的
8.3 课堂练习
语句:class B implements A 也表示 B 继承了 A 的属性
8.4 实现接口 VS 继承类
当子类继承了父类,就自动的拥有父类的功能。如果子类需要扩展功能,可以通过实现接口的方式扩展。可以理解 实现接口 是 对 java 单继承机制 的一种补充
接口与继承解决的问题不同:
- 继承的价值主要在于:解决代码的复用性和可维护性
- 接口的价值主要在于:设计!设计好各种规范(方法),让其它类去实现这些方法。
接口比继承更加灵活
接口在一定程度上实现代码解耦(即:接口规范性 + 动态绑定)
public class Test {
public static void main(String[] args) {
littleMonkey wukong = new littleMonkey("wukong");
wukong.climing(); //通过继承父类可以直接调用父类的方法
wukong.swimming(); //通过实现接口,来扩展方法
wukong.fly();
}
}
class Monkey{
private String name;
public Monkey(String name) {
this.name = name;
}
public void climing(){
System.out.println(getName() + " 学会了爬树......");
}
public String getName() {
return name;
}
}
interface Fishable{
void swimming();
}
interface Birdable{
void fly();
}
class littleMonkey extends Monkey implements Fishable,Birdable{
public littleMonkey(String name) {
super(name);
}
@Override
public void swimming() {
System.out.println(getName() + " 学会了游泳......");
}
@Override
public void fly() {
System.out.println(getName() + " 学会了飞翔......");
}
}
8.5 接口的多态
参考笔记9中的多态的应用。接口的多态主要体现在三个地方:1. 多态参数;2. 多态数组;3. 接口的多态传递
-
多态参数。接口引用可以指向实现了接口的类的对象。
//例子1 public class Test { public static void main(String[] args) { //接口类型的变量 ia 可以指向实现了 IA 接口的类的对象实例 IA ia = new Car(); ia = new Monster(); //对比继承的多态,父类类型的变量 a 可以指向继承AAA的子类的对象实例 AA a = new BB(); a = new CC(); } } interface IA{} class Monster implements IA{} class Car implements IA{} class AA{} class BB extends AA{} class CC extends AA{}
//例子2 public class All { public static void main(String[] args) { //创建手机,相机对象,两者都实现了USB Phone phone = new Phone(); Camera camera = new Camera(); //创建计算机 Computer computer = new Computer(); //接口引用可以指向实现了接口的类的对象 computer.work(phone);//把手机接入到计算机(public void work(USB usb) 所以类似于向上转型) computer.work(camera);//把相机接入到计算机 } } interface USB{ void start(); void stop(); } class Phone implements USB {//Phone 实现了 USB接口 @Override public void start() { System.out.println("Phone start......"); } @Override public void stop() { System.out.println("Phone stop......"); } } class Camera implements USB {//Camera 实现了 USB接口 @Override public void start() { System.out.println("Camera start......"); } @Override public void stop() { System.out.println("Camera stop......"); } } class Computer { //1.USB usb形参是接口类型 USB public void work(USB usb){//usb可以是 Phone和Camera对象的实例,因为他们实现了接口 usb.start(); usb.stop(); } }
-
多态数组。演示一个案例:给USB 数组中存放 Phone 和 Camera 对象,Phone 类还有一个特有的方法 call(),请遍历 USB 数组,如果是 Phone 对象,除了调用 USB 接口定义的方法外,还需要调用 Phone 特有方法 call。
public class Test { public static void main(String[] args) { USB[] usbs = new USB[2]; usbs[0] = new Phone(); usbs[1] = new Camera(); for (int i = 0; i < usbs.length; i++) { usbs[i].work();//遵从动态绑定机制 //和讲多态那里一样,需要进行类型的向下转型 if(usbs[i] instanceof Phone){//判断他的运行类型是Phone ((Phone)usbs[i]).call();//将编译类型转换为Phone才能调用Phone中的特有方法 } } } } interface USB{ void work(); } class Phone implements USB{ @Override public void work() { System.out.println("Phone working......"); } public void call(){ System.out.println("Phone can call......"); } } class Camera implements USB{ @Override public void work() { System.out.println("Camera working......"); } }
-
接口的多态传递现象。接口存在多态传递现象。
public class Test { public static void main(String[] args) { IB ib = new AA(); IA ia = new AA();//只有接口IB继承了接口IA才能这么用 } } interface IA{ void hi(); } interface IB extends IA{} //接口IB继承接口IA class AA implements IB{ @Override public void hi() { } }
8.4 课堂练习
//看看程序中的错误,并改正
public class Test {
}
interface A{
int x = 0;//想到等价于:public final static int x = 0;
}
class B{
int x = 1;
}
class C extends B implements A{
public void pX(){
System.out.println(x);//错误,原因不明确x是谁
}
public static void main(String[] args) {
new C().pX();
}
}
/*
*如果要访问接口A中的x,用:A.x;
*如果要访问父类B中的x,用:super().x
*/
9. 内部类
9.1 内部类的基本介绍
一个类的内部又完整的嵌套了另一个类结构。被嵌套的类称为内部类(inner class),嵌套其他类的类称为外部类(outer class)。类的五大成员:属性、方法、构造器、代码块、内部类。内部类最大的特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系。
基本语法:
class Outer{//外部类
class Inner{//内部类
}
}
class Other{//外部其他类
}
class Outer{//外部类
private int a;//1.属性
public void m(){//2.方法
System.out.println("成员方法...");
}
public Outer(int a) {//3.构造器
this.a = a;
}
{//4.代码块
System.out.println("代码块...");
}
class Inner{//5.内部类
}
}
9.2 内部类的分类
定义在外部类的局部位置上(比如方法内):
- 局部内部类(有类名)
- 匿名内部类(没有类名,十分重要!!!!!!!!!!!!!)
定义在外部类的成员位置上:
- 成员内部类(没有 static 修饰)
- 静态内部类(使用 static 修饰)
9.3 局部内部类
说明:局部内部类是定义在外部类的局部位置,比如定义在方法 or 代码块中,并且有类名
- 可以直接访问外部类的所有成员,包括私有的
- 不能添加访问修饰符,因为它(局部内部类)的地位就是一个局部变量。局部变量是不能用访问修饰符。但是可以使用 final 修饰,因为局部变量也可以使用 final。
- 作用域:仅仅在定义它的方法或代码块中
- 局部内部类访问外部类的成员——直接访问;
- 外部类访问局部内部类的成员——创建对象,再访问(注意必须在作用域内)
- 外部其它类不能访问局部内部类,因为局部内部类地位是一个局部变量
- 如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果局部内部类想访问外部类的成员,则可以使用(外部类名.this.成员 )去访问
记住:①、局部内部类定义在方法/代码块中。②、作用域在方法体或者代码块中。③、本质仍然是一个类
//
public class InnerClass {
public static void main(String[] args) {
Outer outer = new Outer();
outer.m1();//输出800 100
System.out.println("outer hashCode = " + outer);
}
}
class Outer{//外部类
private int n1 = 100;
private void m2(){//私有方法
System.out.println("Outer m2()......");
}
public void m1(){
//1.局部内部类是定义在外部类的局部位置,通常在方法
//3.不能添加访问修饰符,但是可以使用final修饰
//4.作用域:仅仅在定义它的方法或代码块中
final class Inner{
private int n1 = 800;//定义一个跟外部类重名的属性,证明第7条
public void f1(){
//2.局部内部类可以直接访问外部类的所有成员,包括私有
//7.如果外部类和局部内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用Outer.this.n1
//其中 哪个对象调用了m1(), Outer.this就是哪一个对象
System.out.println("内部类的 n1 = " + n1 + "外部类的 n1 = " + Outer.this.n1);
System.out.println("Outer.this hashCode = " + Outer.this);
m2();
}
}
//5.外部类在方法中,可以创建Inner对象,然后调用方法即可
Inner inner = new Inner();
inner.f1();
}
}
9.4 匿名内部类
匿名内部类的使用:①、本质是类;②、内部类;③、该类没有名字(实际上底层创建了,只是一闪而过);④、同时还是一个对象
说明:匿名内部类是定义在外部类的局部位置,比如方法中,并且没有类名。
匿名内部类的使用细节:
- 匿名内部类的基本语法:
new 类或接口(参数列表){
类体
};
public class InnerClass {
public static void main(String[] args) {
Outer outer = new Outer();
outer.method();
}
}
class Outer {//外部类
private int n1 = 10;//属性
public void method() {//方法
//基于接口的匿名内部类
//tiger的编译类型?IA
//tiger的运行类型?即匿名内部类 XXXX => Outer$1
/*
* 我们看底层:会分配 类名:Outer$1(即外部类类名+$1)
* class XXXX implements IA{
@Override //只要重写了方法,就是一个新的东西了
public void cry() {
System.out.println("老虎叫唤......");
};
}
*/
//JDK 底层在创建了匿名内部类 Outer$1 后,立马就创建了 Outer$1 实例(对象),并把地址返回给了 tiger
//匿名内部类使用一次就不能再使用了,但是返回的实例化对象tiger可以重复使用
//其实也相当于匿名内部类执行了IA接口:class Outer$1 implements IA{}
//注意:接口不能被实例化,单独一个 IA tiger = new IA();是会报错的,所以下面这种确实是创建了一个新类
IA tiger = new IA() {
@Override
public void cry() {
System.out.println("老虎叫唤......");
}
};
System.out.println("tiger 的运行类型 = " + tiger.getClass());//getClass()获得运行类型
tiger.cry();
//基于类的匿名内部类
//1.father的编译类型?Father
//2.father的运行类型?Outer$2
//3.底层会创建匿名内部类
/*
class Outer$1 extends Father{
@Override //对于类,匿名内部类中,只要重写了原来类的方法,就是一个新类了,并且继承原类
public void test() {
System.out.println("匿名内部类重写了 test 方法");
}
}
*/
//4.同时也直接返回了匿名内部类 Outer$2的对象
//5.注意("jack")参数列表会传递给构造器
//其实也相当于匿名内部类继承了Father类:class Outer$2 extends Father{}
//如果是Father father = new Father("jack");那么father的运行类型还是Father类,并且没有创建匿名内部类
Father father = new Father("jack") {
@Override
public void test() {//可以不用重写这个方法
System.out.println("匿名内部类重写了 test 方法");
}
};
System.out.println("father对象的运行类型 = " + father.getClass());
father.test();
//基于抽象类的匿名内部类
Animal animal = new Animal() {
@Override
void eat() {//因为此方法是抽象方法,所以必须重写
System.out.println("小狗吃骨头......");
}
};
System.out.println("animal对象的运行类型 = " + animal.getClass());//Outer$3
animal.eat();
}
}
interface IA {//接口
public void cry();
}
class Father {//类
public Father(String name) {//构造器
}
public void test() {//方法
}
}
abstract class Animal{//抽象类
abstract void eat();
}
- 匿名内部类的语法比较奇特,因为匿名内部类既是一个类的定义,同时它本身也是一个对象,因此从语法上看,它既有定义类的特征,也有创建对象的特征,对前面代码分析可以看出这个特点,因此可以调用匿名内部类方法。
public class InnerClass {
public static void main(String[] args) {
Outer outer = new Outer();
outer.f1();//输出:匿名内部类重写了 hi()方法
}
}
class Outer{
private int n1 = 99;
public void f1(){
//创建一个基于类的匿名内部类
Person person = new Person() {//person匿名内部类,运行类型是Outer$1
@Override
public void hi() {
System.out.println("匿名内部类重写了 hi()方法");
}
};
person.hi();//动态绑定:编译类型是Person,运行类型是Outer$1
//也可以直接调用,匿名内部类本身也是返回对象
//相当于class 匿名内部类 extends Person
new Person(){//新的匿名内部类,运行类型Outer$2
@Override
public void hi() {
System.out.println("匿名内部类重写了 hi()方法......");
}
@Override
public void ok(String str) {
super.ok(str);//匿名内部类的父类是Person(对super的解释)
}
}.ok("jack");//输出:Person ok() jack
}
}
class Person{
public void hi(){
System.out.println("Person hi()...");
}
public void ok(String str){
System.out.println("Person ok() " + str);
}
}
/*
匿名内部类重写了 hi()方法
Person ok() jack
*/
- 可以直接访问外部类的所有成员,包括私有成员
- 不能添加访问修饰符,因为匿名内部类的地位就是局部变量(同局部内部类)
- 作用域:仅仅在定义它的方法中或者代码块中
- 匿名内部类访问外部成员——直接访问
- 外部其它类不能访问匿名内部类
- 如果外部类和匿名内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员 )去访问
9.5 匿名内部类的最佳实践
匿名内部类当做实参直接传递,简洁高效
public class InnerClass{
public static void main(String[] args) {
f1(new IA() {
@Override
public void show() {
System.out.println("这是一幅名画...");
}
});
//此句传递的实参是一个匿名内部类返回的对象(即 f1(括号内的所有) ),这个匿名内部类的编译类型:IA,运行类型:InnerClass$1
//并且IA$1重写了show方法
}
//静态方法,形参是接口类型
public static void f1(IA ia){
ia.show();
}
}
interface IA{
void show();
}
public class InnerClass{
public static void main(String[] args) {
CellPhone cellPhone = new CellPhone();
/*
1.传递的是一个实现了Bell接口的匿名内部类,类名为InnerClass$1
2.该匿名内部类重写了ring
3.Bell bell = new Bell() {
@Override
public void ring() {
System.out.println("懒猪起床了");
}
}
即public void alarmclock(Bell bell)中的bell形参接受的实参是上面那一块
bell的编译类型是Bell,运行类型是InnerClass$1
*/
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接口类型
System.out.println(bell.getClass());
bell.ring();//动态绑定
}
}
9.6 成员内部类
说明:成员内部类是定义在外部内的成员位置,并且没有 static 修饰。
-
可以直接访问外部类的所有成员,包括私有的
-
可以添加任何访问修饰符(public,private,默认,protected),因为它的地位就是一个成员
-
作用域:和外部类的其他成员 一样,为整个类体(与前面两个不同)
-
成员内部类访问外部类成员——直接访问
-
外部类访问成员内部类——创建对象,再访问
-
其他外部类访问成员内部类——两种方式,如下代码
-
如果外部类和成员内部类的成员重名时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.this.成员 )去访问
public class InnerClass02 { public static void main(String[] args) { Outer outer = new Outer(); outer.t1();//66 张三 10 //6.外部其他类使用成员内部类的两种方式 //第一种方式: outer.new Inner(); 其中outer是外部类的实例化对象,相当于把 new Inner() 当做是outer的成员 Outer.Inner inner = outer.new Inner(); inner.say();//66 张三 10 //第二种方式,在外部类中编写一个方法,返回一个Inner对象 //注意两种方式的编译类型都是:Outer.Inner Outer.Inner innerinstance = outer.getInnerInstance(); innerinstance.say(); } } class Outer{ private int n1 = 10; public String name = "张三"; //1.注意成员内部类,是定义在外部内的成员位置上 //2.可以添加任何访问修饰符(public、private...),因为它的地位是一个成员 public class Inner{ private int n1 = 66; public void say(){ //可以直接访问外部内的所有成员,包括私有的 System.out.println("n1= " + n1 + " name = " + name + "外部类的n1 = " + Outer.this.n1); } } //返回一个Inner()实例 public Inner getInnerInstance(){ return new Inner(); } //写调用方法 public void t1(){ Inner inner = new Inner(); inner.say();//外部类访问成员内部类的方法:创建对象,再访问 } }
9.7 静态内部类
说明:静态内部类是定义在外部类的成员位置,并且有 static 修饰
- 可以直接访问外部类的所有静态成员,包含私有的,但**不能直接访问非静态成员**
- 可以添加任意访问修饰符(public、protected、默认、private),因为它的地位就是一个成员
- 作用域:同其他的成员,为整个类体
- 静态内部类访问外部类——直接访问所有静态成员
- 外部内访问静态内部类——创建对象,再访问
- 外部其他类访问静态内部类——两种方式
- 如果外部类和静态内部类的成员重名时,静态内部类访问的时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.成员)去访问(这里因为静态内部类只能访问外部类的静态成员,而静态成员的方式是通过 类名.静态成员 的方式里访问的,不用this)
public class InnerClass02 {
public static void main(String[] args) {
Outer outer = new Outer();
outer.m1();
//6.外部其他类使用静态内部类
//方式1:因为静态内部类是可以直接通过类名访问的(前提是满足访问权限)
Outer.Inner inner = new Outer.Inner();
inner.say();
//方式2:写一个方法,可以返回静态内部类的对象实例
Outer.Inner inner1 = outer.getInner();//这是对象.getInner(),这个getInner()是一个非静态方法
inner1.say();
Outer.Inner inner_ = Outer.getInner_();//直接通过 外部类名.(静态)返回内部类方法
inner_.say();
}
}
class Outer{
private int n1 = 10;
public static String name = "张三";
private static void cry(){};
//Inner就是静态内部类
//1.方在外部类的成员位置
//2.使用static修饰
//4.可以任意添加访问修饰符(public、prvate...)
public static class Inner {
private static String name = "张二娃";
public void say(){
//3.可以直接访问外部类的所有 静态 成员,包括私有的,但是不能访问非静态成员
//因为这里外部类的name是静态的,所以直接用 Outer.name访问
System.out.println("静态内部类的name = " + name + " 外部类的name = " + Outer.name);
cry();
}
}
//5.作用域:同其他成员,为整个类体
public void m1(){
Inner inner = new Inner();
inner.say();
}
//返回内部类对象实例
public Inner getInner(){
return new Inner();
}
public static Inner getInner_(){
return new Inner();
}
}
9.8 课堂练习
输出为:5,5
ate),因为它的地位就是一个成员
3. 作用域:同其他的成员,为整个类体
4. 静态内部类访问外部类——直接访问所有静态成员
5. 外部内访问静态内部类——创建对象,再访问
6. 外部其他类访问静态内部类——两种方式
7. 如果外部类和静态内部类的成员重名时,静态内部类访问的时,默认遵循就近原则,如果想访问外部类的成员,则可以使用(外部类名.成员)去访问(这里因为静态内部类只能访问外部类的静态成员,而静态成员的方式是通过 类名.静态成员 的方式里访问的,不用this)
public class InnerClass02 {
public static void main(String[] args) {
Outer outer = new Outer();
outer.m1();
//6.外部其他类使用静态内部类
//方式1:因为静态内部类是可以直接通过类名访问的(前提是满足访问权限)
Outer.Inner inner = new Outer.Inner();
inner.say();
//方式2:写一个方法,可以返回静态内部类的对象实例
Outer.Inner inner1 = outer.getInner();//这是对象.getInner(),这个getInner()是一个非静态方法
inner1.say();
Outer.Inner inner_ = Outer.getInner_();//直接通过 外部类名.(静态)返回内部类方法
inner_.say();
}
}
class Outer{
private int n1 = 10;
public static String name = "张三";
private static void cry(){};
//Inner就是静态内部类
//1.方在外部类的成员位置
//2.使用static修饰
//4.可以任意添加访问修饰符(public、prvate...)
public static class Inner {
private static String name = "张二娃";
public void say(){
//3.可以直接访问外部类的所有 静态 成员,包括私有的,但是不能访问非静态成员
//因为这里外部类的name是静态的,所以直接用 Outer.name访问
System.out.println("静态内部类的name = " + name + " 外部类的name = " + Outer.name);
cry();
}
}
//5.作用域:同其他成员,为整个类体
public void m1(){
Inner inner = new Inner();
inner.say();
}
//返回内部类对象实例
public Inner getInner(){
return new Inner();
}
public static Inner getInner_(){
return new Inner();
}
}