六.面向对象编程(高级)
1.类变量和类方法
-
类变量就是类里边的静态变量/静态属性
- 它是属于类的,而不是属于某个特定的对象。对于一个类中的不同对象来说,类变量只有一个且是可以共享的。(*举个例子,假设有一个学生类,类中有学生名字和此学生进去之后图书馆当前已经进的总人数(限制人数是100人),每个学生都是一个学生类的对象,这些学生对象进入的都是同一个图书馆,每来一个学生进入,类中的这个图书馆人数类变量就要++,从这个例子里体现类变量的作用*)
//类变量声明,在类中 //类变量可以在其他类中通过类名来访问,也可以使用对象实例访问 class Student { private String name; //定义一个变量 count ,是一个类变量(静态变量) static 静态 //该变量最大的特点就是会被 Student 类的所有的对象实例共享 public static int count = 0; }
-
类变量内存布局
- 根据JDK,类变量可能会放在不同位置(堆空间、方法去静态域中),但是不影响所有对象都是共享同一个类变量,且在类加载中就生成了
-
类变量定义和访问
class A { public static String name = "njm"; //推荐访问修饰符放在前面 static public String name = "njm"; } class B { //通过类名调用类变量,不需要预先创建对象实例,但要注意访问权限 System.out.println(A.name);//推荐用类名调用 //通过对象调用类变量 A a = new A(); System.out.println(a.name); }
-
类变量使用细节
- 什么时候需要类变量:需要让所有对象都共享一个变量时
- 类变量与实例变量(普通属性)的区别:实例变量是每个对象独享的,实例变量不能通过
类名.实例变量
来调用 - 加上static称为类变量和静态变量,否则称为实例变量、普通变量、非静态变量、非静态成员变量
- 类变量的声明周期是随类的加载开始,随着类的消亡而销毁
-
类方法就是静态方法,静态方法可以访问静态属性,非静态方法也可以访问静态属性
-
类方法使用细节
- 什么时候需要类方法:当方法中不涉及到任何和对象相关的成员,则可以将方法设置成静态方法,提高开发效率(不创建实例也能调用方法,即把方法当成工具类使用时)
- 类方法中不可以使用和对象有关的关键字,比如this、super
- 类方法中只能用静态变量和静态方法;普通方法既可以访问普通变量/方法,也可以访问静态变量/方法
2.理解main函数入口
public static void main(String[] args) { }
- main方法时虚拟机来调用,所以必须是public才能运行!!
- Java虚拟机在执行main()时不创建对象,所以main必须是static(使用类名直接调用)
- main接受String类型的数组参数,该数组中保存执行Java命令时传递给所运行类的参数
- 在命令行运行java时,后边直接跟参数(可变长)即可
- 静态方法main要访问本类的非静态成员,需要先创建对象,再调用即可
- main的动态传值:在IDEA设置一下,使每次点击运行时都自动传数组参数,具体设置自行百度吧
3.代码块
-
代码块又叫初始化块,属于类中的成员,类似于方法,将逻辑封装在{}中,但和方法不同的是没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或者类显示调用,而是在加载类或者创建对象时隐式调用
[修饰符]{ 代码 }; //1.最后分号可有可无 //2.修饰符要么不屑,要么是static //3.static修饰的角静态代码块,没有其修饰的是普通代码块/非静态代码块
-
应用场景:如果多个构造器中都存在重复的语句,可以抽取到初始化块中,提高代码的重用性
-
使用细节
-
静态代码块的作用就是对类初始化,随着类加载而执行,且只执行一次;普通代码块,每创建一个新对象就执行
-
(⭐)类什么时候被加载
- 创建对象实例时
- 创建子类对象实例,父类也被加载,而且父类先被加载
- 使用类的静态成员(静态属性、静态方法)
-
普通代码块,在创建对象实例时,会被隐式的调用,创建一个对象就调用一次;如果只是使用类的静态成员时,普通代码块并不会执行
-
(⭐)创建一个对象时,类中的调用顺序
- 调用静态代码块和静态属性初始化(这两个没有优先级,按照定义的顺序)
- 调用普通代码块和普通属性初始化(这两个没有优先级,按照定义的顺序)
- 调用类的构造方法
-
构造器的最前面隐含了super()和调用普通代码块,静态相关的代码块,属性初始化,在类加载时,就执行完毕,因此是优先于构造器和普通代码块执行的
class A { public A() { //构造器 //这里有隐藏的执行要求 //(1)super() //(2)调用普通代码块 // 构造器的代码 } }
- (⭐)创建一个子类对象时,类中的调用顺序
- 父类调用静态代码块和静态属性初始化(这两个没有优先级,按照定义的顺序)
- 子类调用静态代码块和静态属性初始化(这两个没有优先级,按照定义的顺序)
- 父类调用普通代码块和普通属性初始化(这两个没有优先级,按照定义的顺序)
- 父类的构造方法
- 子类调用普通代码块和普通属性初始化(这两个没有优先级,按照定义的顺序)
- 子类的构造方法
- 静态代码块只能调用静态成员(静态属性和静态方法);普通代码块可以调用任意成员
-
4.单例设计模式
-
设计模式:大量的实践中总结和理论化之后优选的代码结构、编程风格、以及解决问题的思考方式
-
单例设计模式:每个类只能存在一个对象实例,并且该类只提供一个取得其对象实例的方法
- 饿汉式:
class GirlFriend private String name; //public static int n1 = 100; //为了能够在静态方法中,返回 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; } }
- 懒汉式
class Cat { private String name; public static int n1 = 999; private static Cat cat ; //默認是null //步驟 //1.仍然構造器私有化 //2.定義一個static靜態屬性對象 //3.提供一個public的static方法,可以返回一個Cat對象 //4.懶漢式,只有當用戶使用getInstance時,才返回cat對象, 後面再次調用時,會返回上次創建的cat對象 // 從而保證了單例 private Cat(String name) { System.out.println("構造器調用..."); this.name = name; } public static Cat getInstance() { if(cat == null) {//如果還沒有創建cat對象 cat = new Cat("小可愛"); } return cat; } }
-
饿汉式VS懒汉式
- 创建对象时机不同,饿汉式是类加载就创建,懒汉式是在使用时才创建
- 饿汉式不存在线程安全问题,懒汉式存在线程安全问题
5.final关键字
-
基本介绍
-
不希望类被继承时,可以用final修饰
-
不希望父类的某个方法被子类覆盖/重写时,可以用final
public final void f(){}
-
不希望类的某个属性值被修改时,可以用final
-
不希望方法中的某个局部变量值被修改时,可以用final
-
-
使用细节
- final修饰的属性又叫常量,一半用XX_XX_XX命名
- final修饰的属性必须在定义时赋初值,并且不能再修改,赋值可以在以下位置之一(注意只能选一个)
- 定义时直接赋值
- 构造器中
- 代码块中
- 如果final修饰的属性是静态的,则舒适化的位置只能时以下位置之一(注意只能选一个)
- 定义时直接赋值
- 静态代码块中
- final类不能继承,但可以实例化
- 类不是final,但是含有final方法,该方法虽然不能重写,但是可以被继承
- 如果一个类是final类,没必要再将里边的方法修饰成final
- final不能修饰构造器
- 由于构造方法的独特性质,它已经具备了某种意义上的“最终性”,即每个类自己有自己的构造器,且自己的多个构造器之间不存在覆盖,所以谈不上继承、重写的事儿
- final和static搭配使用(谁先谁后都可以),效率更高
- 因为底层编译器做了优化处理,调用属性不会导致类加载
- 包装类和String都是final类
6.抽象类
- 当父类的某些方法需要声明,但是又不确定如何实现时,可以把它声明为抽象方法,那么这个类就是抽象类
abstract class A { //抽象类
private String name;
public A (String name) {
this.name = name;
}
public abstract void eat() ; //抽象方法,没有方法体
}
- 一个抽象方法必须在抽象类中,抽象类中不一定有抽象方法,抽象类中还可以有普通方法
- 抽象类的价值在于设计,设计好之后让子类继承并实现。是考官喜欢提问的知识点,在框架和设计模式中使用较多
- 抽象类使用细节
- 抽象类不能被实例化,但可以有任意成员(非抽象方法、构造器、静态属性等等)
- abstract只能修饰类和方法,不能修饰属性和其它的
- 抽象方法不能有主体(即不能有{})
- 一个类继承了抽象类,则它必须实现抽象类的所有抽象方法
- 抽象类不能使用private、final、static来修饰,因为这些关键字都是和重写相违背的
- private私有的不能被子类重写
- final根本不能被继承,更别提重写
- static关键字与方法重写无关
7.抽象类的实践-模版设计模式
-
模版设计模式是23种基本模式之一,利用到了抽象类,抽象类作为多个子类的通用模版,子类在抽象类的基础上进行扩展、改造,但子类总体上回保留抽象类的行为方式
-
模版设计模式能解决的问题
- 当内部功能的一部分是确定的,一部分是不确定的,就可以把不确定的部分暴露出去,由子类去实现
-
实践
- 要求:(1)有多个类,分别完成各自不同的任务 (2)要求统计得到各自任务的完成时间
- 思考:要求1是根据每个任务的不同,需要编写不同的处理代码,而要求2在各自任务中都是记录任务开始和任务结束时间并做减法,所以把执行任务写成抽象方法暴露给各子类,而将记录时间写在父类(模板类)中
- 代码:
package com.hspedu.abstract_; abstract public class Template { //抽象类-模板设计模式 public abstract void job();//抽象方法 public void calculateTime() {//实现方法,调用 job 方法 //得到开始的时间 long start = System.currentTimeMillis(); job(); //动态绑定机制 //得的结束的时间 long end = System.currentTimeMillis(); System.out.println("任务执行时间 " + (end - start)); } }
package com.hspedu.abstract_; public class AA extends Template { //计算任务 //1+....+ 800000 @Override public void job() { //实现 Template 的抽象方法 job long num = 0; for (long i = 1; i <= 800000; i++) { num += i; } } } package com.hspedu.abstract_; public class BB extends Template{ public void job() {//这里也去,重写了 Template 的 job 方法 long num = 0; for (long i = 1; i <= 80000; i++) { num *= i; } } }
package com.hspedu.abstract_; public class TestTemplate { public static void main(String[] args) { AA aa = new AA(); aa.calculateTime(); //这里还是需要有良好的 OOP 基础,对多态 BB bb = new BB(); bb.calculateTime(); } }
- 上述代码中注意程序的运行顺序:aa.calculateTime() -> Template.calculateTime() -> aa.job
- 从子类AA中找不到calculateTime()方法,就向上查找,找到了父类的calculateTime()方法并执行,执行过程中调用了job()方法,注意有动态绑定机制,所以调用方法一定是先从运行类型开始找,即父类的emplate.calculateTime()执行子类AA的aa.job方法
8.接口
-
接口就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,再根据具体情况把这些方法写出来
interface 接口名{ //属性 //抽象方法 } class 类名 implements 接口{ //自己属性 //自己方法 //必须实现的接口的抽象方法 }
-
接口的使用细节
- 接口不能被实例化
- 接口中所有方法都是public方法,接口中抽象方法可以不用abstract修饰
void aaa(); //实际上是abstract void aaa();
-
一个普通类实现接口,必须将该接口的所有方法实现;抽象类实现接口,可以不用实现接口的方法
- 当接口中方法过多时,可将光标放在接口上按alt+enter,可一次性将所有接口中方法实现
-
一个类同时可以实现多个接口
-
接口中属性是public static final的
int a = 1; // 实际上是 public static final int a = 1; //必须初始化
- 接口中属性的访问形式:接口名.属性名
- 接口不能继承类,但可以继承多个别的接口
- 接口的修饰符只能是public和默认,这点和类的修饰符是一样的
-
练习
interface A { int a = 23; } class B implements A { } main中: B b = new B(); //ok System.out.println(b.a); //23 System.out.println(A.a); //23 System.out.println(B.a); //23
-
接口和抽象类的对比
参数 抽象类 接口 方法定义 抽象类中既可以包含抽象方法,也可以包含非抽象方法(即有具体实现的方法) 接口中的所有方法默认都是抽象的(在Java 8之前),即只有方法签名而无方法体,Java 8之后,接口还可以包含静态方法(static methods)和默认方法(default methods),这两者都可以有方法体的实现 方法实现 子类使用extends来继承,子类如果不是抽象类,必须实现抽象类中所有抽象方法 子类通过implements实现接口,需要提供接口中所有方法的实现 构造器 可以有 不能有 继承 单继承 可以多继承 成员变量及方法 有多种访问修饰符 private、protected或者public 变量默认是public static final的,即接口常量;方法默认是public 速度 比接口速度快 接口稍微慢,因为需要时间去寻找在类中的实现方法 解决问题 继承类解决代码的复用性和可维护性 接口的截止主要在于设计,设计好各种规范,让其他类实现 -
一个类继承父类和实现接口可以理解为本能和技能
-
接口的多态特性
-
多态参数
- 当方法定义时参数是一个接口时,实际传进去的参数可以是任何实现了此接口的类实例对象
-
多态数组
package com.hspedu.interface_; 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("相机工作中..."); } }
-
接口多态传递现象
package com.hspedu.interface_; /** * 演示多态传递现象 */ public class InterfacePolyPass { public static void main(String[] args) { //接口类型的变量可以指向,实现了该接口的类的对象实例 IG ig = new Teacher(); //如果IG 继承了 IH 接口,而Teacher 类实现了 IG接口 //那么,实际上就相当于 Teacher 类也实现了 IH接口. //这就是所谓的 接口多态传递现象. IH ih = new Teacher(); } } interface IH { void hi(); } interface IG extends IH{ } class Teacher implements IG { @Override public void hi() { } }
-
9.内部类(⭐重难点!!!)
-
类的五大成员:(1)变量/属性(2)方法(3)构造器(4)代码块(5)内部类
-
一个类的内部又完整的嵌套了另一个类,被嵌套的类称为内部类(inner class),嵌套其他类的类称为外部类(outer class),内部类最大特点就是可以直接访问私有属性,并且体现类之间的包含关系
class Outer { //外部类 class Inner { //内部类 } }
-
内部类的分类
- 定义在外部类局部位置上(比如方法内):
- 局部内部类(有类名)
- 匿名内部类(没有类名,⭐重点)
- 定义在外部类的成员位置上:
- 成员内部类(没有用static修饰)
- 静态内部类(使用static修饰)
- 定义在外部类局部位置上(比如方法内):
-
局部内部类
- 定义:定义在外部类的局部位置,比如方法、代码块中
- 可以直接访问外部类的所有成员,包含私有的,因为本质上就是在这个类中使用的private成员
- 不能添加访问修饰符,因为他是一个局部变量,局部变量不需要被外界调用,只需要在局部位置(方法或者代码块)中使用,但可以给局部内部类添加final修饰,将不能被修改
- 作用域:定义它的代码块或者方法中
- 局部内部类访问外部类成员----->直接访问
- 外部类访问局部内部类成员----->创建对象再访问(必须在作用域中)
- 外部其他类不能访问局部内部类(因为局部内部类本质是局部变量)
- 如果外部类和局部内部类的成员重名,默认就近原则,如果局部内部类想访问外部类的成员,可以用(外部类.this.成员)
class Outer02 {//外部类 private int n1 = 100; private void m2() { System.out.println("Outer02 m2()"); }//私有方法 public void m1() {//方法 final class Inner02 {//局部内部类(本质仍然是一个类) private int n1 = 800; public void f1() { // 老韩解读 Outer02.this 本质就是外部类的对象, 即哪个对象调用了m1, Outer02.this就是哪个对象 System.out.println("n1=" + n1 + " 外部类的n1=" + Outer02.this.n1); System.out.println("Outer02.this hashcode=" + Outer02.this); m2(); } } } }
-
匿名内部类(⭐)
- 本质是一个继承父类方法的子类或者实现接口的类,而且是内部类,该类没有名字,使用父类或者接口来声明编译类型即可使用(例如下边的tiger对象,编译类型是接口,运行类型是匿名内部类)
- 基于接口的匿名内部类
class Outer04 { //外部类 private int n1 = 10;//属性 public void method() {//方法 //基于接口的匿名内部类 //老韩解读 //1.需求: 想使用IA接口,并创建对象 //2.传统方式,是写一个类,实现该接口,并创建对象 //3.老韩需求是 Tiger/Dog 类只是使用一次,后面再不使用 //4. 可以使用匿名内部类来简化开发 //5. tiger的编译类型 ? IA //6. tiger的运行类型 ? 就是匿名内部类 Outer04$1 /* 我们看底层 会分配 类名 Outer04$1 class Outer04$1 implements IA { @Override public void cry() { System.out.println("老虎叫唤..."); } } */ //7. jdk底层在创建匿名内部类 Outer04$1,立即马上就创建了 Outer04$1实例,并且把地址 // 返回给 tiger //8. 匿名内部类使用一次,就不能再使用 //tiger编译类型:IA 运行类型:匿名内部 IA tiger = new IA() { @Override public void cry() { System.out.println("老虎叫唤..."); } }; System.out.println("tiger的运行类型=" + tiger.getClass()); tiger.cry(); tiger.cry(); tiger.cry();//匿名内部类不能多次使用,但实例可以多次使用 } interface IA {//接口 public void cry(); }
- 基于类的匿名内部类
class Outer04 { //外部类 private int n1 = 10;//属性 public void method() {//方法 //演示基于类的匿名内部类 //分析 //1. father编译类型 Father //2. father运行类型 Outer04$2 //3. 底层会创建匿名内部类 /* class Outer04$2 extends Father{ @Override public void test() { System.out.println("匿名内部类重写了test方法"); } } */ //4. 同时也直接返回了 匿名内部类 Outer04$2的对象 //5. 注意("jack") 参数列表会传递给 构造器 Father father = new Father("jack"){ @Override public void test() { System.out.println("匿名内部类重写了test方法");//只要重写了就不再是本类,而是继承类,在这里是匿名内部类 } }; System.out.println("father对象的运行类型=" + father.getClass());//Outer04$2 father.test(); } } class Father {//类 public Father(String name) {//构造器 System.out.println("接收到name=" + name); } public void test() {//方法 } }
- 基于抽象类的匿名内部类
class Outer04 { //外部类 private int n1 = 10;//属性 public void method() {//方法 //基于抽象类的匿名内部类 Animal animal = new Animal(){ @Override void eat() { System.out.println("小狗吃骨头..."); } }; animal.eat(); } } abstract class Animal { //抽象类 abstract void eat(); }
-
匿名内部类的细节
- 两种使用方法
class Outer05 { private int n1 = 99; public void f1() { //1.可以用父类接收子类对象 Person p = new Person(){ private int n1 = 88; @Override public void hi() { System.out.println("匿名内部类重写了 hi方法 n1=" + n1 + " 外部内的n1=" + Outer05.this.n1 ); //Outer05.this 就是调用 f1的 对象,这里就是Outer05类的对象 System.out.println("Outer05.this hashcode=" + Outer05.this); } }; p.hi();//动态绑定, 运行类型是 Outer05$1 //2.也可以直接调用, 匿名内部类本身也是返回对象 // class 匿名内部类 extends Person {} new Person(){ @Override public void hi() { System.out.println("匿名内部类重写了 hi方法,哈哈..."); } }.hi; } } class Person {//类 public void hi() { System.out.println("Person hi()"); } }
- 匿名内部类语法奇特,因为它既是一个类的定义,同时它本身也是一个对象。从语法角度看,既有定义类的特征,也有创建对象的特征。
- 可以直接访问外部类的所有成员,包含私有的
- 不能添加访问修饰符,因为本质是局部变量(局部变量没必要加访问修饰符)
- 作用域:方法或者代码块中
- 外部其他类不能访问匿名内部类(因为匿名内部类本质是局部变量)
- 如果外部类和匿名内部类的成员重名,默认就近原则,如果匿名内部类里想访问外部类的成员,可以用(外部类.this.成员)
-
匿名内部类的最佳实践
- 当作实参直接传递,简洁高效
public void f1( new IL(){ //IL是个接口 @override public void shouw() { System.out.println("这是匿名内部类作为实参"); } }) { //f1函数的方法体 }
-
成员内部类
-
定义在外部类中的成员位置上,可以直接访问外部类的所有成员,包括私有的
-
可以添加任意访问修饰符,因为它就是一个成员
-
作用域:和外部类的其他成员一样,作用域为整个类
-
成员内部类访问外部类成员------>直接访问
-
外部类访问成员内部类-------->创建成员内部类对象,再访问
-
外部其他类访问成员内部类------->两种方式
- 先创建外部类对象,再用外部类对象创建内部类对象
// 第一种方式 // outer08.new Inner08(); 相当于把 new Inner08()当做是outer08成员 // 这就是一个语法,不要特别的纠结. Outer08 outer08 = new Outer08(); Outer08.Inner08 inner08 = outer08.new Inner08();
- 外部类中,编写方法,在外部其他类中创建外部类对象,然后调用此方法
// 第二方式 在外部其他类中创建外部类对象 Outer08 outer08 = new Outer08();//外部类对象 Outer08.Inner08 inner08Instance = outer08.getInner08Instance(); //外部类中的调用方法 public Inner08 getInner08Instance(){ return new Inner08(); }
-
如果外部类和成员内部类的成员重名,默认就近原则,如果成员内部类里想访问外部类的成员,可以用(外部类.this.成员)
-
-
静态内部类
-
定义在外部类中的成员位置上,可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员
-
可以添加任意访问修饰符,因为它就是一个成员
-
作用域:和外部类的其他成员一样,作用域为整个类
-
静态内部类访问外部类成员------>直接访问所有静态成员(只能访问静态成员)
-
外部类访问静态内部类-------->创建静态内部类对象,再访问
-
外部其他类访问静态内部类------->两种方式
//方式1 //因为静态内部类,是可以通过类名直接访问(前提是满足访问权限) Outer10.Inner10 inner10 = new Outer10.Inner10(); inner10.say(); //方式2 //编写一个方法,可以返回静态内部类的对象实例. Outer10 outer10 = new Outer10();//外部类对象 Outer10.Inner10 inner101 = outer10.getInner10(); inner101.say(); //getInner10()定义 public Inner10 getInner10() { return new Inner10(); } //方式2的简化 //不创建外部类对象直接调用方法,将方法设成static Outer10.Inner10 inner10_ = Outer10.getInner10_(); System.out.println("************"); inner10_.say(); //Inner10 getInner10_() 定义 public static Inner10 getInner10_() { return new Inner10(); }
-
如果外部类和静态内部类的成员重名,默认就近原则,如果静态内部类里想访问外部类的成员,可以用(外部类.成员)
-