前言
哈喽!大家好,我是小简。今天开始学习《Java-面向对象》,此系列是我做的一个 “Java 从 0 到 1 ” 实验,给自己一年左右时间,按照我自己总结的 Java-学习路线,从 0 开始学 Java 知识,并不定期更新所学笔记,期待一年后的蜕变吧!<有同样想法的小伙伴,可以联系我一起交流学习哦!>
面向对象-基础
类与对象
成员方法
方法递归
方法重载
可变参数
作用域
构造方法
对象创建
this关键字
面向对象-中级
包
访问修饰符
封装
继承
super关键字
重写&重载
多态
多态引出
请编写一个程序,Master 类中有一个 feed 方法, 可以完成主人给动物喂食物的信息。
使用传统的方法来解决带来的问题是什么? 如何解决?
public class Poly_ {
public static void main(String[] args) {
Master tom = new Master("Tom");
Dog jack = new Dog("jack");
Cat jerry = new Cat("jerry");
Bone bone = new Bone("大骨头");
Fish fish = new Fish("小鱼");
tom.feed(jack,bone);
tom.feed(jerry,fish);
}
}
class Master {
private String name;
public Master(String name) {
this.name = name;
}
//主人给小狗喂食骨头
public void feed(Dog dog, Bone bone) {
System.out.println("主人" + name + " 给" + dog.getName() + " 吃" + bone.getName());
}
//主人给小猫喂黄花鱼
public void feed(Cat cat, Fish fish) {
System.out.println("主人" + name + " 给" + cat.getName() + " 吃" + fish.getName());
}
}
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class Dog extends Animal {
public Dog(String name) {
super(name);
}
}
class Cat extends Animal {
public Cat(String name) {
super(name);
}
}
class Food {
private String name;
public Food(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class Bone extends Food {
public Bone(String name) {
super(name);
}
}
class Fish extends Food {
public Fish(String name) {
super(name);
}
}
带来的问题:
- 如果动物很多,食物很多,feed 方法很多,不利于管理和维护
- 代码的复用性不高,而且不利于代码维护
解决方案: 采用多态解决
public class Poly_ {
public static void main(String[] args) {
Master tom = new Master("Tom");
Dog jack = new Dog("jack");
Cat jerry = new Cat("jerry");
Bone bone = new Bone("大骨头");
Fish fish = new Fish("小鱼");
tom.feed(jack,bone);
tom.feed(jerry,fish);
}
}
class Master {
private String name;
public Master(String name) {
this.name = name;
}
//使用多态机制,可以统一的管理主人喂食的问题
//animal 编译类型是Animal,可以指向(接收) Animal 子类的对象
//food 编译类型是Food ,可以指向(接收) Food 子类的对象
public void feed(Animal animal, Food food) {
System.out.println("主人" + name + " 给" + animal.getName() + " 吃" + food.getName());
}
}
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class Dog extends Animal {
public Dog(String name) {
super(name);
}
}
class Cat extends Animal {
public Cat(String name) {
super(name);
}
}
class Food {
private String name;
public Food(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class Bone extends Food {
public Bone(String name) {
super(name);
}
}
class Fish extends Food {
public Fish(String name) {
super(name);
}
}
多态介绍
方法或对象具有多种形态。是面向对象的第三大特征,多态是建立在封装和继承基础之上的。
重载和重写都体现了多态
一个对象的编译类型和运行类型可以不一致
编译类型在定义对象时,就确定了,不能改变
运行类型是可以变化的
编译类型看定义时=号的左边,运行类型看=号的右边
Animal animal = new Dog();//animal编译类型是Animal,运行类型Dog animal = new Cat(); //animal的运行类型变成了Cat, 编译类型仍然是Animal
多态的前提是:两个对象(类)存在继承关系
多态的向上转型
1)本质:父类的引用指向了子类的对象
2)语法:父类类型 引用名 = new 子类类型();
Animal animal = new Dog();
3)特点:编译类型看左边,运行类型看右边
- 可以调用父类中的所有成员(需遵守访问权限)
- 不能调用子类中特有成员(属性/方法)
- 最终运行效果看子类的具体实现
public class PolyDetail {
public static void main(String[] args) {
//向上转型: 父类的引用指向了子类的对象
Animal_ animal = new Cat_();
//可以调用父类中的所有成员(需遵守访问权限)
animal.eat();//吃
animal.sleep();//睡
//不能调用子类中特有成员(属性/方法)
animal.catchMouse();//错误写法
}
}
class Animal_ {
String name = "动物";
int age = 10;
public void sleep(){
System.out.println("睡");
}
public void eat(){
System.out.println("吃");
}
}
class Cat_ extends Animal_ {
@Override
public void eat(){//方法重写
System.out.println("猫吃鱼");
}
public void catchMouse(){//Cat 特有方法
System.out.println("猫抓老鼠");
}
}
多态的向下转型
1)语法:子类类型引用名 = (子类类型) 父类引用;
Dog dog = (Dog) animal;
2)只能强转父类的引用,不能强转父类的对象
3)要求父类的引用必须指向的是当前目标类型的对象
4)当向下转型后,可以调用子类类型中所有的成员
public class PolyDetail {
public static void main(String[] args) {
Animal_ animal = new Cat_();
//向下转型:希望可以调用Cat的catchMouse 方法
Cat_ cat = (Cat_) anima;
cat.eat();//吃
cat.sleep();//睡
cat.catchMouse();//特有方法也可调用
//要求父类的引用必须指向的是当前目标类型的对象
Dog dog = (Dog) animal; //错误写法
}
}
class Animal_ {
String name = "动物";
int age = 10;
public void sleep(){
System.out.println("睡");
}
public void eat(){
System.out.println("吃");
}
}
class Cat_ extends Animal_ {
@Override
public void eat(){//方法重写
System.out.println("猫吃鱼");
}
public void catchMouse(){//Cat 特有方法
System.out.println("猫抓老鼠");
}
}
class Dog_ extends Animal_ {//Dog 是Animal 的子类
}
属性重写问题
属性没有重写之说!属性的值看编译类型
public class PolyDetail02 {
public static void main(String[] args) {
//属性没有重写之说!属性的值看编译类型
Base base = new Sub();//向上转型
System.out.println(base.count);//看编译类型10
Sub sub = new Sub();
System.out.println(sub.count);//20
}
}
class Base { //父类
int count = 10;//属性
}
class Sub extends Base {//子类
int count = 20;//属性
}
instanceOf
用于判断对象的运行类型是否为XX 类型或XX 类型的子类型
public class PolyDetail03 {
public static void main(String[] args) {
BB bb = new BB();
System.out.println(bb instanceof BB);//true
System.out.println(bb instanceof AA);//true
//BB 是AA 子类
AA aa = new BB(); //aa 编译类型AA, 运行类型是BB
System.out.println(aa instanceof AA);//true
System.out.println(aa instanceof BB);//true
Object obj = new Object();
System.out.println(obj instanceof AA);//false
String str = "hello";
System.out.println(str instanceof Object);//true
}
}
class AA {} //父类
class BB extends AA {}//子类
动态绑定机制
- 当调用对象方法的时候,该方法会和该对象的运行类型(内存地址)绑定
- 当调用对象属性时,没有动态绑定机制,哪里声明,那里使用
public class DynamicBinding {
public static void main(String[] args) {
//a 的编译类型A, 运行类型B
A a = new B();//向上转型
//运行类型是B,先找B类里是否有sum,有就直接运行
System.out.println(a.sum());//40 调用子类sum
System.out.println(a.sum1());//30 调用子类sum1
}
}
class A {//父类
public int i = 10;
public int sum() {//父类sum()
return getI() + 10;
}
public int sum1() {//父类sum1()
return i + 10;
}
public int getI() {//父类getI
return i;
}
}
class B extends A {//子类
public int i = 20;
@Override
public int sum() {
return i + 20;
}
@Override
public int getI() {//子类getI()
return i;
}
@Override
public int sum1() {
return i + 10;
}
}
public class DynamicBinding {
public static void main(String[] args) {
//a 的编译类型A, 运行类型B
A a = new B();//向上转型
//运行类型是B,先找B类里是否有sum,没有就去父类找
System.out.println(a.sum());//30 调用父类sum 和子类getI
System.out.println(a.sum1());//20 调用父类sum 和父类i
}
}
class A {//父类
public int i = 10;
//动态绑定机制
public int sum() {//父类sum()
return getI() + 10;//20 + 10
}
public int sum1() {//父类sum1()
return i + 10;//10 + 10
}
public int getI() {//父类getI
return i;
}
}
class B extends A {//子类
public int i = 20;
@Override
public int getI() {//子类getI()
return i;
}
}
多态的应用
多态数组
数组的定义类型为父类类型,里面保存的实际元素类型为子类类型
应用实例:现有一个继承结构如下:要求创建1 个Person 对象、2 个Student 对象和2 个Teacher 对象, 统一放在数组中,并调用每个对象 say 方法,如何调用子类特有的方法,比如 Teacher 有一个 teach , Student 有一个 study
怎么调用?
public class PolyArray {
public static void main(String[] args) {
//应用实例:现有一个继承结构如下:要求创建1 个Person 对象、
// 2 个Student 对象和2 个Teacher 对象, 统一放在数组中,并调用每个对象say 方法
Person[] persons = new Person[5];
persons[0] = new Person("jack", 20);
persons[1] = new Student("mary", 18, 100);
persons[2] = new Student("smith", 19, 30.1);
persons[3] = new Teacher("scott", 30, 20000);
persons[4] = new Teacher("king", 50, 25000);
//循环遍历多态数组,调用say
for (int i = 0; i < persons.length; i++) {
//person[i] 编译类型是Person ,运行类型是是根据实际情况有JVM 来判断
System.out.println(persons[i].say());//动态绑定机制
// 使用类型判断+ 向下转型
if(persons[i] instanceof Student) {//判断person[i] 的运行类型是不是Student
Student student = (Student)persons[i];//向下转型
student.study();
//也可以使用一条语句((Student)persons[i]).study();
} else if(persons[i] instanceof Teacher) {
Teacher teacher = (Teacher)persons[i];
teacher.teach();
} else if(persons[i] instanceof Person){
System.out.println("不做处理...");
} else {
System.out.println("你的类型有误, 请自己检查...");
}
}
}
}
class Person {//父类
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public String say() {//返回名字和年龄
return name + "\t" + age;
}
}
class Student extends Person {
private double score;
public Student(String name, int age, double score) {
super(name, age);
this.score = score;
}
//重写父类say
@Override
public String say() {
return "学生" + super.say() + " score=" + score;
}
//特有的方法
public void study() {
System.out.println("学生" + getName() + " 正在学java...");
}
}
class Teacher extends Person {
private double salary;
public Teacher(String name, int age, double salary) {
super(name, age);
this.salary = salary;
}
//写重写父类的say 方法
@Override
public String say() {
return "老师" + super.say() + " salary=" + salary;
}
//特有方法
public void teach() {
System.out.println("老师" + getName() + " 正在讲java 课程...");
}
}
多态参数
方法定义的形参类型为父类类型,实参类型允许为子类类型
应用实例: 定义员工类 Employee,包含姓名和月工资[private],以及计算年工资 getAnnual
的方法。普通员工和经理继承了员工,经理类多了奖金 bonus 属性和管理 manage 方法,普通员工类多了 work 方法,普通员工和经理类要求分别重写 getAnnual 方法,测试类中添加一个方法 showEmpAnnual(Employee e),实现获取任何员工对象的年工资,并在 main 方法中调用该方法[e.getAnnual()] 测试类中添加一个方法,testWork,如果是普通员工, 则调用 work 方法,如果是经
理,则调用 manage方法
public class PolyParameter {
public static void main(String[] args) {
Worker tom = new Worker("tom", 2500);
Manager milan = new Manager("milan", 5000, 200000);
PolyParameter.showEmpAnnual(tom);
PolyParameter.showEmpAnnual(milan);
PolyParameter.testWork(tom);
PolyParameter.testWork(milan);
}
public static void showEmpAnnual(Employee e) {
System.out.println(e.getAnnual());
}
public static void testWork(Employee e) {
if (e instanceof Worker) {
((Worker) e).work();//有向下转型操作
} else if (e instanceof Manager) {
((Manager) e).manage();//有向下转型操作
} else {
System.out.println("不做处理...");
}
}
}
class Employee {
private String name;
private double salary;
public Employee(String name, double salary) {
this.name = name;
this.salary = salary;
}
public String getName() {
return name;
}
public double getSalary() {
return salary;
}
public double getAnnual() {
return 12 * salary;
}
}
class Worker extends Employee {
public Worker(String name, double salary) {
super(name, salary);
}
public void work() {
System.out.println("普通员工" + getName() + " is working");
}
@Override
public double getAnnual() {
return super.getAnnual();
}
}
class Manager extends Employee {
private double bonus;
public Manager(String name, double salary, double bonus) {
super(name, salary);
this.bonus = bonus;
}
public void manage() {
System.out.println("经理" + getName() + " is managing");
}
@Override
public double getAnnual() {
return super.getAnnual() + bonus;
}
}
Object类
equals
equals 是 Object 类中的方法,只能判断引用类型,默认判断的是地址是否相等,子类中往往重写该方法,用于判断内容是否相等。比如 Integer,String
== 和 equals 的对比
== 是一个比较运算符
1.既可以判断基本类型,又可以判断引用类型
2.如果判断基本类型,判断的是值是否相等。
3.如果判断引用类型,判断的是地址是否相等,即判定是不是同一个对象
equals 是 Object 类中的方法,只能判断引用类型,默认判断的是地址是否相等
如何重写 equals 方法
🌰:判断两个 Person 对象的内容是否相等,如果两个 Person 对象的各个属性值都一样,则返回true,反之false
public class EqualsExercise01 {
public static void main(String[] args) {
Person person1 = new Person("jack", 10, '男');
Person person2 = new Person("jack", 20, '男');
System.out.println(person1.equals(person2));//假
}
}
//判断两个Person 对象的内容是否相等,
//如果两个Person 对象的各个属性值都一样,则返回true,反之false
class Person{ //extends Object
private String name;
private int age;
private char gender;
//重写Object 的equals 方法
public boolean equals(Object obj) {
//判断如果比较的两个对象是同一个对象,则直接返回true
if(this == obj) {
return true;
}
//类型判断
if(obj instanceof Person) {//是Person,我们才比较
//进行向下转型, 因为我需要得到obj 的各个属性
Person p = (Person)obj;
return this.name.equals(p.name) && this.age == p.age && this.gender == p.gender;
}
//如果不是Person ,则直接返回false
return false;
}
public Person(String name, int age, char gender) {
this.name = name;
this.age = age;
this.gender = gender;
}
}
hashCode
返回该对象的哈希码值。此方法是为了提高哈希表的性能。
两个引用,如果指向的是同一个对象,则哈希值肯定是一样的!
两个引用,如果指向的是不同对象,则哈希值是不一样的。
哈希值主要根据地址号来的!,但是 不能完全将哈希值等价于地址。
集合中 hashCode 如果需要的话,也会重写。
toString
默认返回:全类名(包名+类名)+@+哈希值的十六进制
子类往往重写toString 方法,用于返回对象的属性信息
重写 toString 方法,打印对象或拼接对象时,都会自动调用该对象的toString 形式
当直接输出一个对象时, toString 方法会被默认的调用
- 比如System.out.println(monster) ; 就会默认调用 monster.toString()
finalize
当对象被回收时,系统自动调用该对象的 finalize 方法。子类可以重写该方法,做一些释放资源的操作
什么时候被回收:当某个对象没有任何引用时,则 jvm 就认为这个对象是一个垃圾对象,就会使用垃圾回收机制来销毁该对象,在销毁该对象前,会先调用finalize 方法。
垃圾回收机制的调用,是由系统来决定(即有自己的GC 算法), 也可以通过System.gc() 主动触发垃圾回收机制
public class Finalize_ {
//如果程序员不重写finalize,那么就会调用Object类的finalize, 即默认处理
//如果程序员重写了finalize, 就可以实现自己的逻辑
public static void main(String[] args) {
Car bmw = new Car("宝马");
//这时car对象就是一个垃圾
//垃圾回收器就会回收(销毁)对象,
//在销毁对象前,会调用该对象的finalize 方法
bmw = null;
System.gc();//主动调用垃圾回收器
System.out.println("程序退出了....");
}
}
class Car {
private String name;
public Car(String name) {
this.name = name;
}
//重写finalize
@Override
protected void finalize() throws Throwable {
System.out.println("我们销毁汽车" + name );
System.out.println("释放了某些资源...");
}
}
面向对象-高级
类变量(静态变量)
类变量简介
类变量(又叫静态变量)是该类的所有对象共享的变量
,任何一个该类的对象去访问它时,取到的都是相同的值,同样任何一个该类的对象去修改它时,修改的也是同一个变量。
类变量内存布局
jdk8 及以前版本,放在静态域;
jdk8 以后版本,放在堆中,当你的类加载的时候会在堆中生成一个类的 class 对象,静态变量就在 class 对象尾部。
类变量特点
- 类变量是同一个类所有对象实例共享的
- 类变量,在类加载的时候就生成了
- (即类加载的时候就会初始化类变量,即使没有创建类对象实例也可通过类名访问)
- 类变量的生命周期是随类的加载开始,随着类的消亡而销毁。
类变量的定义
- 访问修饰符 static 数据类型 变量名【推荐使用】
- static 访问修饰符 数据类型 变量名
类变量的访问
- 类名.类变量名【推荐使用】
- 对象名.类变量名
类变量使用场景
当我们需要让某个类的所有对象都共享一个变量时,就可以考虑使用类变量(静态变量)。比如:定义学生类,统计所有学生共交多少钱。Student (name, static fee)
类变量与普通变量
- 类变量是该类的所有对象共享的
- 实例变量(普通变量/类成员变量/非静态变量)是每个对象独享的
代码理解类变量
public class ChildGame {
public static void main(String[] args) {
//普通方法需定义一个变量 count, 统计有多少小孩加入了游戏
//int count = 0;
Child child1 = new Child("白骨精");
child1.join();
//count++;
child1.count++;
Child child2 = new Child("狐狸精");
child2.join();
//count++;
child2.count++;
Child child3 = new Child("老鼠精");
child3.join();
//count++;
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; //该变量最大的特点就是会被 Child 类的所有的对象实例共享
public Child(String name) {
this.name = name;
}
public void join() {
System.out.println(name + " 加入了游戏..");
}
}
类方法(静态方法)
类方法,无需创建类的对象即可对其进行访问
类方法定义
- 访问修饰符 static 数据返回类型 方法名(){}【推荐使用】
- static 访问修饰符 数据返回类型 方法名(){}
类方法访问
- 类名.类方法名【推荐使用】
- 对象名.类方法名
类方法使用场景
- 当方法中不涉及到任何和对象相关的成员,则可以将方法设计成静态方法,提高开发效率。
- 比如工具类中的方法 Utils, Math类、Arrays类、Collections集合类
- 在实际开发中,往往将一些通用的方法,设计成静态方法,这样我们不需要创建对象就可以使用,比如打印一维数组,冒泡排序等等
类方法与普通方法
- 类方法和普通方法都是随着类的加载而加载,将结构信息存储在方法区:类方法中无 this 的参数,普通方法中隐含着 this 的参数。
- 类方法可以通过类名或对象名调用,而普通方法只能通过对象名调用。
- 类方法中不允许使用和对象有关的关键字,比如
this
、super
;普通方法可以。(因为当用类名.类方法名
调用的时候,this 和 super 指示不明) - 类方法中只能访问静态变量或静态方法;而普通方法既可以访问普通变量(方法),也可以访问静态变量(方法)。
- 类方法可以被子类继承,但是不能被子类重写
代码理解类方法
package com.jwt.static_;
public class StaticMethod {
public static void main(String[] args) {
//创建两个学生交学费
Stu tom = new Stu("Tom");
tom.payFee(100);
Stu mary = new Stu("mary");
mary.payFee(200);
Stu.showFee();
}
}
class Stu{
private String name;
private static double fee = 0;//静态变量累积学费
public Stu(String name) {
this.name = name;
}
public static void payFee(double fee){//静态方法可访问静态变量
Stu.fee += fee;
}
public static void showFee(){
System.out.println("总学费:" + Stu.fee);
}
}
main方法
深入理解main方法
main 方法的形式: public static void main(String[] args){}
public:main 方法是 JVM 调用的,该方法的访问权限必须是 public;
static:Java虚拟机在执行main()方法时不必创建对象,所以该方法必须是 static;
args:该方法接收 String 类型的数组参数,该数组中保存执行 Java 命令时传递给所运行的类的参数
特别注意
在 main() 方法中,我们可以直接调用 main 方法所在类的静态方法或静态属性。
但是,不能直接访问该类中的非静态成员,必须创建该类的一个实例对象后,才能通过这个对象去访问类中的非静态成员。
IDEA 中给main传参数
第一步 | 第二步 |
---|---|
代码块
简介
代码块又称为 初始化块,属于类中的成员,类似于方法,将逻辑语句封装在方法体中,通过 { } 包围起来,但和方法不同,没有方法名,没有返回,没有参数,只有方法体,而且不用通过对象或类显式调用,而是加载类时或创建对象时隐式调用。
代码块语法
[修饰符]{
这里是代码;
};
说明:
- 修饰符可选,要写的话,也只能写 static
- 代码块分为两类,使用 static 修饰的叫静态代码块,没有 static 修饰的,叫普通代码块
- 块中代码可以为任何逻辑语句(输入、输出、方法调用、循环、判断等)
- 分号,可以写上,也可以省略
代码块的好处
首先看下面代码,三个构造器都有相同的语句,这样代码看起来比较冗余,如何简化代码呢?
public class CodeBlock01 {
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;
//3个构造器-》重载
public Movie(String name) {
System.out.println("电影屏幕打开...");
System.out.println("广告开始...");
System.out.println("电影正是开始...");
System.out.println("Movie(String name) 被调用...");
this.name = name;
}
public Movie(String name, double price) {
System.out.println("电影屏幕打开...");
System.out.println("广告开始...");
System.out.println("电影正是开始...");
this.name = name;
this.price = price;
}
public Movie(String name, double price, String director) {
System.out.println("电影屏幕打开...");
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;
}
}
这时我们可以把相同的语句,放入到一个代码块中,这样当我们不管调用哪个构造器创建对象,都会先调用代码块的内容,代码块调用的顺序优先于构造器
{
System.out.println("电影屏幕打开...");
System.out.println("广告开始...");
System.out.println("电影正是开始...");
};
改进后代码:
public class CodeBlock01 {
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("电影正是开始...");
};
//3个构造器-》重载
public Movie(String name) {
System.out.println("Movie(String name) 被调用...");
this.name = name;
}
public Movie(String name, double price) {
this.name = name;
this.price = price;
}
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;
}
}
总结
- 代码块相当于另外一种形式的构造器(对构造器的补充机制),可以做初始化的操作。
- 如果多个构造器中都有重复的语句,可以抽取到初始化块中,提高代码的复用性
重点
static 代码块也叫静态代码块,作用就是对类进行初始化,而且它随着类的加载而执行,并且只会执行一次。
类什么时候被加载
- 创建对象实例时(new)
- 创建子类对象实例,父类也会被加载
- 使用类的静态成员时(静态方法,静态属性)
普通的代码块,在创建对象实例时,会被隐式的调用。被创建一次,就会调用一次。
如果只是使用类的静态成员时,普通代码块并不会执行。
创建一个对象时,它们的调用顺序是:
- ➀首先调用静态代码块和静态属性初始化(注意:静态代码块和静态属性初始化调用的优先级一样,如果有多个静态代码块和多个静态变量初始化,则按他们定义的顺序调用)
- ➁再调用普通代码块和普通属性的初始化(注意:普通代码块和普通属性初始化调用的优先级一样,如果有多个普通代码块和多个普通属性初始化,则按定义顺序调用)
- ➂最后调用构造方法。
构造器的最前面其实隐含了 super() 和调用普通代码块。
静态代码块只能直接调用静态成员(静态属性和静态方法),普通代码块可以调用任意成员
创建一个子类对象时,在一个类调用顺序是:
- ➀ 父类的静态代码块和静态属性初始化(优先级一样,按定义顺序执行)
- ➁ 子类的静态代码块和静态属性初始化(优先级一样,按定义顺序执行)
- ➂ 父类的普通代码块和普通属性初始化(优先级一样,按定义顺序执行)
- ➃ 父类的构造方法
- ➄ 子类的普通代码块和普通属性初始化
- ➅ 子类的构造方法
总结:
1——根据static代码块是在类加载时被调用,当要创建子类这个对象时,发现这个类需要一个父类,所以把父类的 .class 加载进来,故先是父类的静态代码块或静态属性初始化;
2——然后是加载子类的类信息,故是子类的静态代码块或静态属性初始化;
3——然后走进子类构造器的super语句,进入父类的构造器,先super(),然后调用父类普通代码块,故是父类的普通代码块或普通属性初始化;
4——然后父类构造器调用完毕,即是父类的构造方法;
5——然后回到子类的构造方法中隐含的调用普通代码块或普通属性的初始化;
6——然后完成子类的构造器,故最后是子类的构造方法。
单例模式
饿汉式
懒汉式
final关键字
final 在 Java 中的意思是最终,也可以称为完结器,表示对象是最终形态的,不可改变的意思。final 可以修饰类、 属性、方法和局部变量,在某些情况下,就会使用到 final:
- 当不希望类被继承时,可以用 final 修饰
- 当不希望父类的某个方法被子类覆盖/重写(override)时可以用 final 关键字修饰
- 当不希望类的的某个属性的值被修改,可以用 final 修饰
- 当不希望某个局部变量被修改,可以使用 final 修饰
//1.如果我们要求A 类不能被其他类继承
//可以使用final 修饰A 类
final class A {
}
//class B extends A {} //会报错
class C {
//2.如果我们要求hi 不能被子类重写
//可以使用final 修饰hi 方法
public final void hi() {
}
}
class D extends C {
// @Override //会报错
// public void hi() {
// System.out.println("重写了C 类的hi 方法..");
// }
}
//3.当不希望类的的某个属性的值被修改,可以用final 修饰
class E {
public final double TAX_RATE = 0.08;//常量
}
//4.当不希望某个局部变量被修改,可以使用final 修饰
class F {
public void cry() {
//这时,NUM 也称为局部常量
final double NUM = 0.01;
//NUM = 0.9; //会报错
System.out.println("NUM=" + NUM);
}
}
注意事项
final 修饰的属性又叫常量,一般用 XX_XX_XX 来命名
final 修饰的属性在定义时,必须赋初值,并且以后不能再修改,赋值可以在如下位置之一:
①定义时
public final double TAX RATE = 0.08;//定义时赋值
②在构造器中
public AA() {//构造器中赋值 TAX_RATE2 = 1.1; }
③在代码块中
{//在代码块赋值 TAX_RATE3 = 8.8; }
final 修饰的属性是静态的,则初始化的位置只能是
①定义时、在静态代码块
public static final double TAX_RATE = 99.9; static { TAX_RATE2 = 3.3; }
②不能在构造器中赋值
final 类不能继承,但是可以实例化对象。
如果类不是 final 类,但是含有 final 方法,则该方法虽然不能重写,但是可
以被继承。一般来说,如果一个类已经是 final 类了,就没有必要再将方法修饰成final方法。
final 不能修饰构造方法(即构造器)。
final 和 static 往往搭配使用,效率更高,不会导致类加载底层编译器做了优化处理。
包装类(Integer,Double,Float, Boolean等都是 final ),String 也是 final 类。
抽象类
抽象类引出
当父类的某些方法,需要声明,但是又不确定如何实现时,可以将其声明为抽象方法, 那么这个类就是抽象类
class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
//eat这里实现了,其实没有什么意义
public void eat() {
System.out.println("这是一个动物,但是不知道吃什么..");
}
}
考虑将该方法设计为抽象(abstract)方法,所谓抽象方法就是没有实现的方法,所谓没有实现就是指,没有方法体。当一个类中存在抽象方法时,需要将该类声明为 abstract 类, 一般来说,抽象类会被继承,由其子类来实现抽象方法。
abstract class Animal {
private String name;
public Animal(String name) {
this.name = name;
}
//只声明不实现
public abstract void eat() ;
}
抽象类介绍
当父类的一些方法不能确定时,可以用 abstract 关键字来修饰该方法,这个方法
就是抽象方法,并且需要用 abstract 来修饰该类,这个类就叫抽象类。
1、用 abstract 关键字来修饰一个类时,这个类就叫抽象类
访问修饰符 abstract 类名{}
2、用 abstract 关键字来修饰一个方法时,这个方法就是抽象方法
访问修饰符 abstract 返回类型 方法名(参数列表);//不能有方法体
3、抽象类的价值更多作用是在于设计,是设计者设计好后,让子类继承并实现抽象类()
抽象类注意事项
- 1、抽象类不能被实例化
- 2、抽象类不一定要包含 abstract 方法。 也就是说,抽象类可以没有 abstract 方法
- 3、一旦类包含了 abstract 方法,则这个类必须声明为 abstract
- 4、abstract 只能修饰类和方法,不能修饰属性和其它的
- 5、如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,除非它自己也声明为 abstract 类。
- 6、抽象方法不能使用 private,final 和 static 来修饰,因为这些关键字都是和重写相违背的。
- 7、抽象类可以有任意成员[抽象类本质还是类],比如:非抽象方法、构造器、静态属性等等
- 8、抽象方法不能有主体,即不能实现
public class AbstractDetail01 {
public static void main(String[] args) {
//1、抽象类,不能被实例化
new A();//错误写法
}
}
//2、抽象类不一定要包含abstract方法。也就是说,抽象类可以没有abstract方法
abstract class A {
public void hi() {
System.out.println("hi");
}
}
//3、一旦类包含了abstract 方法,则这个类必须声明为abstract
abstract class B {
public abstract void hi();
}
//4、abstract 只能修饰类和方法,不能修饰属性和其它的
class C {
public abstract int n1 = 1; //错误写法
}
//5、如果一个类继承了抽象类,则它必须实现抽象类的所有抽象方法,
除非它自己也声明为 abstract 类。
class D extens B {
@Override
public void hi() {
//这里相等于D子类实现了父类B的抽象方法,所谓实现方法,就是有方法体
}
}
//6、抽象方法不能使用 private, final 和 static 来修饰,因为这些关键字都是和重写相违背的。
abstract class E {
private abstract void hi();//错误写法
public final abstract void hi();//错误写法
public static abstract void hi();//错误写法
}
//7、抽象类的本质还是类,所以可以有类的各种成员
abstract class F {
public int n1 = 10;
public static String name = "测试";
public void hi() {
System.out.println("hi");
}
public abstract void hello();
public static void ok() {
System.out.println("ok");
}
}
//8、抽象方法不能有主体,即不能实现
abstract class G {
public abstract void hello(){//错误写法
}
}
抽象类最佳实践-模板设计模式
抽象类体现的就是一种模板模式的设计,抽象类作为多个子类的通用模板,子类在抽象类的基础上进行扩展、改造,但子类总体上会保留抽象类的行为方式。
模板设计模式能解决的问题
- 1、当功能内部一部分实现是确定,一部分实现是不确定的。这时可以把不确定的部分暴露出去,让子类去实现。
- 2、编写一个抽象父类,父类提供了多个子类的通用方法,并把一个或多个方法留给其子类实现,就是一种模板模式。
栗子🌰:
- 1.有多个类,完成不同的任务 job
- 2.要求统计得到各自完成任务的时间
请编程实现 TestTemplate.java
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));
}
}
public class AA extends Template {
//计算任务1
@Override
public void job() { //实现Template 的抽象方法job
long num = 0;
for (long i = 1; i <= 800000; i++) {
num += i;
}
}
}
public class BB extends Template {
//计算任务2
@Override
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();
}
}
接口
接口简介
接口就是给出一些没有实现的方法,封装到一起,到某个类要使用的时候,再根据具体情况把这些方法写出来。
语法:
//定义接口
interface 接口名{
属性;//实际上是 public static final 属性;
抽象方法; //实际上是 public abstract 抽象方法;
//接口中抽象方法可以不用 abstract 关键字
}
//实现接口
class 类名 implements 接口{
自己属性;
自己方法;
必须实现的接口的抽象方法
}
接口是更加抽象的抽象类,抽象类里的方法可以有方法体,接口里的所有方法都没有方法体[jdk7.0]。
接口体现了程序设计的多态和高内聚低偶合的设计思想。
特别说明: Jdk8.0 后接口类可以有静态方法,默认方法,也就是说接口中可以有方法的具体实现。
接口注意事项
- 1、接口不能被实例化
- 2、接口中所有的方法是 public 方法,接口中抽象方法,可以不用 abstract 修饰
- 3、一个普通类实现接口,就必须将该接口的所有方法都实现。
- 4、抽象类实现接口,可以不用实现接口的方法。
public class InterfaceDetail01 {
public static void main(String[] args) {
//1.接口不能被实例化
new IA();//错误写法
}
}
//2.接口中所有的方法是public 方法, 接口中抽象方法,可以不用abstract 修饰
interface IA {
void say();//修饰符默认 public
void hi();//实际就是 public abstract void hi();
}
//3.一个普通类实现接口,就必须将该接口的所有方法都实现,可以使用alt+enter 来解决
class Cat implements IA{
@Override
public void say() {
}
@Override
public void hi() {
}
}
//4.抽象类去实现接口时,可以不实现接口的抽象方法
abstract class Tiger implements IA {
}
- 5、一个类同时可以实现多 个接口
- 6、接口中的属性,只能是 final 的,而且是 public static final 修饰符。
- 比如:int a= 1;实际上是 public static final int a=1; (必须初始化)
- 7、接口中属性的访问形式:接口名.属性名
- 8、接口不能继承其它的类,但是可以继承多个别的接口
- interface A extends B,C{}
- 9、接口的修饰符只能是 public 和默认,这点和类的修饰符是一样的。
public class InterfaceDetail02 {
public static void main(String[] args) {
//接口中的属性,是public static final
System.out.println(IB.n1);//说明n1 就是static
//IB.n1 = 30; 说明n1 是final
}
}
interface IB {
//接口中的属性,只能是final 的,而且是public static final 修饰符
int n1 = 10; //等价public static final int n1 = 10;
void hi();
}
//接口的修饰符只能是public 和默认,这点和类的修饰符是一样的
interface IC {
void say();
}
//接口不能继承其它的类,但是可以继承多个别的接口
interface ID extends IB,IC {
}
//一个类同时可以实现多个接口
class Pig implements IB,IC {
@Override
public void hi() {
}
@Override
public void say() {
}
}
接口&继承
当子类继承了父类,就自动的拥有父类的功能
如果子类需要扩展功能,可以通过实现接口的方式扩展
可以理解实现接口是对Java 单继承机制的一种补充
public class ExtendsVsInterface {
public static void main(String[] args) {
LittleMonkey wuKong = new LittleMonkey("悟空");
wuKong.climbing();
wuKong.swimming();
wuKong.flying();
}
}
//猴子
class Monkey {
private String name;
public Monkey(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void climbing() {
System.out.println(name + " 会爬树...");
}
}
//接口
interface Fishable {
void swimming();
}
interface Birdable {
void flying();
}
class LittleMonkey extends Monkey implements Fishable,Birdable {
public LittleMonkey(String name) {
super(name);
}
@Override
public void swimming() {
System.out.println(getName() + " 通过学习,可以像鱼儿一样游泳...");
}
@Override
public void flying() {
System.out.println(getName() + " 通过学习,可以像鸟儿一样飞翔...");
}
}
/*
悟空 会爬树...
悟空 通过学习,可以像鱼儿一样游泳...
悟空 通过学习,可以像鸟儿一样飞翔...
*/
接口和继承解决的问题不同
- 继承的价值主要在于:解决代码的复用性和可维护性。
- 接口的价值主要在于:设计,设计好各种规范(方法),让其它类去实现这些方法,即更加的灵活。
- 接口比继承更加灵活,继承是满足 is-a 的关系,而接口只需满足 like-a 的关系。
- 接口在一定程度上实现代码解耦(代码相互依赖)[即:接口规范性+动态绑定机制]
接口的多态特性
- 1)多态参数
接口类型的变量可以指向实现了该接口类的对象实例
例如 USB 接口,既可以接收手机对象,又可以接收相机对象,就体现了接口多态(接口引用可以指向实现了接口的类的对象)
public class InterfacePolyParameter {
public static void main(String[] args) {
//接口的多态体现
//接口类型的变量usb可以指向实现了USB接口类的对象实例
USB usb = new Phone();
usb = new Camera();
//继承体现的多态
//父类类型的变量father可以指向继承Farher的子类的对象实例
Farher father = new Son();
father = new Daughter();
}
}
interface USB {}
class Phone implements USB{}
class Camera implements USB{}
class Farher {}
class Son extends Farher {}
class Daughter extends Farher {}
- 2)多态数组
演示案例: 给 USB 数组中,存放 Phone 和相机对象, Phone 类还有一个特有的方法 call() ,请遍历 USB 数组,如果是 Phone 对象,除了调用 USB 接口定义的方法外,还需要调用 Phone 特有方法call
public class InterfacePolyArr {
public static void main(String[] args) {
USB[] usb = new USB[2];
usb[0] = new Phone();
usb[1] = new Camera();
for(int i = 0; i < usb.length; i++) {
usb[i].work();//动态绑定..
//需要进行类型的向下转型
if(usb[i] instanceof Phone) {//判断他的运行类型是Phone_
((Phone) usb[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("相机工作中...");
}
}
- 3)多态传递
如果一个类 Teacher 实现了接口 IG ,IG 接口继承了 IH 接口,那么,实际上就相当于Teacher 类也实现了 IH 接口,这就是接口多态传递现象
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() {
}
}
练习:判断代码是否有误并改进
public class InterfaceExercise02 {
public static void main(String[] args) {
new C().pX();
}
}
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就使用A.x
//访问父类的x就使用super.x
System.out.println(A.x + " " + super.x);
}
}
内部类
简介
一个类的内部又完整的嵌套了另一个类结构。被嵌套的类称为内部类(inner class),嵌套其他类的类称为外部类(outer class)。
内部类最大的特点就是可以直接访问私有属性,并且可以体现类与类之间的包含关系
//语法
class Outer{ //外部类
class Inner{ //内部类
}
}
class Other{ //外部其他类
}
类的五大成员:[属性、方法、构造器、代码块、内部类]
分类
定义在局部位置(方法中/代码块) :
- (1) 局部内部类(有类名)
- (2) 匿名内部类(没有类名)
定义在成员位置:
- (1) 成员内部类(没用 static 修饰)
- (2) 静态内部类(使用 static 修饰)
局部内部类
局部内部类是定义在外部类的局部位置(方法中或者代码块中),并且有类名。
1.不能添加访问修饰符,因为它的地位就是一个局部变量。局部变量是不能使用修饰符的。但是可以使用 final 修饰,因为局部变量也可以使用 final
2.作用域:仅仅在定义它的方法或代码块中。
3.局部内部类访向外部类的成员
- 访问方式:可以直接访问外部类的所有成员,包含私有的
4.外部类访向局部内部类的成员
- 访问方式:创建局部内部类的对象,再访问(注意:必须在作用域内)
5.外部其他类不能访向局部内部类(因为局部内部类地位是一个局部变量)
6.如果外部类和局部内部类的成员重名时
- 默认遵循就近原则
- 如果想访问外部类的成员,则可以使用(外部类名.this.成员)去访问
package com.jwt.innerclass;
public class LocalInnerClass {
public static void main(String[] args) {
Outer outer = new Outer();
outer.m1();
}
}
class Outer {//外部类
private int n1 = 100;
private int n2 = 700;
//私有方法
private void m2() {
System.out.println("外部类的私有方法 m2()");
}
//方法
public void m1() {
//1.不能添加访问修饰符,但是可以使用final 修饰
//2.作用域: 仅仅在定义它的方法或代码块中
final class Inner {//局部内部类
private int n1 = 800;//与外部类成员重名
public void f1() {
// 3.可以直接访问外部类的所有成员,包含私有的
System.out.println("n2=" + n2 );
m2();
//6. 如果外部类和局部内部类的成员重名时,
//1)默认遵循就近原则,
System.out.println("n1=" + n1 );
// 2)如果想访问外部类的成员,使用(外部类名.this.成员)去访问
System.out.println("n1=" + Outer.this.n1);
}
}
//4. 外部类访向局部内部类的成员,创建局部内部类的对象,再访问
Inner inner = new Inner();
inner.f1();
System.out.println("外部类访问局部内部类的n1=" + inner.n1);
}
}
匿名内部类
匿名内部类是定义在外部类的局部位置(方法中或者代码块中),并且没有类名
- 匿名内部类本质是类
- 同时还是一个对象
匿名内部类的基本语法
new 类或接口(参数列表){
类体
};
匿名内部类的语法比较奇特, 匿名内部类既是一个类的定义,同时它本身也是一个对象,因此从语法上看,它既有定义类的特征,也有创建对象的特征
- 1.不能添加访问修饰符,因为它的地位就是一个局部变量。
- 2.作用域:仅仅在定义它的方法或代码块中。
- 3.匿名内部类访向外部类成员
- 访问方式:可以直接访问外部类的所有成员,包含私有的
- 4.外部其他类不能访匿名内部类(因为匿名内部类地位是一个局部变量)
- 5.如果外部类和匿名内部类的成员重名时,匿名内部类访问的话
- 默认遵循就近原则
- 如果想访问外部类的成员,则可以使用(外部类名.this.成员) 去访问
package com.jwt.innerclass;
public class AnonymousInnerClass {
public static void main(String[] args) {
Outer05 outer05 = new Outer05();
outer05.f1();
}
}
class Outer05 {
private int n1 = 100;
private int n2 = 700;
public void f1() {
//1.不能添加访问修饰符
//2.作用域: 仅仅在定义它的方法或代码块中
Person p = new Person() {//创建一个基于类的匿名内部类
private int n1 = 800;//与外部类成员重名
@Override
public void hi() {
//3.可以直接访问外部类的所有成员,包含私有的
System.out.println("n2=" + n2 );
//5.如果外部类和匿名内部类的成员重名时,匿名内部类访问的话
//1)默认遵循就近原则
System.out.println("n1=" + n1 );
//2)如果想访问外部类的成员,则可以使用(外部类名.this.成员)去访问
System.out.println("n1=" + Outer05.this.n1 );
}
};
p.hi();//动态绑定
//也可以直接调用, 匿名内部类本身也是返回对象
new Person(){
@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);
}
}
匿名内部类的最佳实践:当做实参直接传递,简洁高效
package com.jwt.innerclass;
public class InnerClassExercise01 {
public static void main(String[] args) {
//当做实参直接传递,简洁高效
f1(new IL() {
@Override
public void show() {
System.out.println("这是一副名画~~...");
}
});
//传统方法
f1(new Picture());
}
//静态方法,形参是接口类型
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...");
}
}
练习:
1.有一个铃声接口 Bell,里面有个 ring 方法。
2.有一个手机类 Cellphone, 具有闹钟功能 alarmclock,参数是 Bell 类型
3.测试手机类的闹钟功能,通过匿名内部类(对象)作为参数,打印:懒猪起床了
4.再传入另一个匿名内部类(对象),打印:小伙伴上课了
package com.jwt.innerclass;
public class InnerClassExercise02 {
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){
System.out.println(bell.getClass());
bell.ring();//动态绑定
}
}
成员内部类
成员内部类是定义在外部类的成员位置,并且没有 static 修饰。
- 1.可以添加任意访问修饰符(public、protected 、默认、private),因为它的地位就是一个成员。
- 2.作用域:和外部类的其他成员一样,为整个类体
- 3.成员内部类访向外部类成员(比如:属性)
- 访问方式:可以直接访问外部类的所有成员,包含私有的
- 4.外部类访问成员内部类
- 访问方式:创建对象, 再访问
- 5.外部其他类访问成员内部类
//方式一:外部类名.成员内部类名 对象名 = 外部类名.new 成员内部类名()
Outer.Inner inner = Outer.new Inner()
//方式二:在外部类中编写一个方法,返回成员内部类对象
public class Inner{
...
public Inner getInnerInstance(){
return new Inner();
}
}
Outer outer = new Outer()
outer.getInnerInstance()
- 6.如果外部类和内部类的成员重名时, 内部类访问的话
- 默认遵循就近原则
- 如果想访问外部类的成员,则可以使用(外部类名.this.成员)去访问
静态内部类
静态内部类是定义在外部类的成员位置,并且有 static 修饰。
- 1.可以添加任意访问修饰符(public、 protected、 默认、private),因为它的地位就是一个成员。
- 2.作用域:同其他的成员,为整个类体
- 3.静态内部类访问外部类(比如:静态属性)
- 访问方式:可以直接访问外部类的所有静态成员,包含私有的,但不能直接访问非静态成员
- 4.外部类访问静态内部类
- 访问方式:创建对象,再访问
- 5.外部其他类访问静态内部类
//方式一:外部类名.成员内部类名 对象名 = 外部类名.new 成员内部类名()
Outer.Inner inner = new Outer.Inner()
//方式二:在外部类中编写一个方法,返回成员内部类对象
static class Inner{
...
public Inner getInnerInstance(){
return new Inner();
}
}
Outer outer = new Outer()
outer.getInnerInstance()
//或者
Outer.getInnerInstance()
- 6.如果外部类和静态内部类的成员重名时,静态内部类访问的时,
- 默认遵循就近原则
- 如果想访问外部类的成员,则可以使用(外部类名.成员) 去访问
❤️Sponsor
您的支持是我不断前进的动力,如果您恰巧财力雄厚,又感觉本文对您有所帮助的话,可以考虑打赏一下本文,用以维持本博客的运营费用,拒绝白嫖,从你我做起!🥰🥰🥰
支付宝支付 | 微信支付 |