Java面向对象基础(下)
- 理解动态多态性的表现
- 理解向上转型与向下转型的概念
- 掌握instanceof关键字的使用
- 掌握匿名内部类的实现方式
- 理解静态内部类与非静态内部类的区别
- 了解局部内部类形式
- 掌握枚举类的声明和使用
- 了解记录类的特点和声明方式
- 了解密封类的特点和声明方式
- 了解代码块的作用和执行特点
7.1 多态
7.1.1 什么是多态
多态是继封装、继承之后,面向对象的第三大特性。
多态的本意就是多种形态,在编程中通常是指方法的多种形态。多态有两种形式:静态多态(也称为编译时多态,如方法重载)和动态多态(也称为运行时多态,如方法重写)。动态多态是通过继承和接口实现的,允许我们使用父类或接口类型的引用指向子类对象,然后调用方法时,会根据对象的实际类型来决定调用哪个方法的实现。这样可以编写出更加灵活和可扩展的代码。
7.1.2 动态多态性
想要呈现动态多态性,需要3个必要条件:
- 子类继承父类或实现接口
- 方法重写
- 多态引用
1、多态引用的语法
Java规定父类型的变量可以接收子类类型的对象,这一点从逻辑上也是说得通的。
父类类型 变量名 = 子类对象;
父接口类型 变量名 = 实现类对象;
2、动态多态性的表现
多态引用后调用方法的表现是:编译时看左边,运行时看右边。这就是Java虚方法的动态绑定机制。所谓虚方法,就是可以被子类/实现类重写的方法。
package com.atguigu.polymorphism.grammar;
public class Pet {
public void eat(){
System.out.println("吃东西");
}
}
package com.atguigu.polymorphism.grammar;
public class Dog extends Pet {
public void watchHouse(){
System.out.println("看家");
}
@Override
public void eat(){
System.out.println("狗狗啃骨头");
}
}
package com.atguigu.polymorphism.grammar;
public class Cat extends Pet{
public void catchMouse(){
System.out.println("抓老鼠");
}
@Override
public void eat(){
System.out.println("猫咪吃鱼仔");
}
}
package com.atguigu.polymorphism.grammar;
public class Pig extends Pet {
}
package com.atguigu.polymorphism.grammar;
public class TestPolymorphismGood {
public static void main(String[] args) {
Pet p = new Dog();
p.eat();
// pet.watchHouse();//不能调用子类扩展的方法
p = new Cat();
p.eat();
p = new Pig();
p.eat();
}
}
运行结果:
狗狗啃骨头
猫咪吃鱼仔
吃东西
3、如何获取对象的运行时类型呢?
java.lang.Object类中有一个方法可以获取对象的运行时类型:
public final Class getClass()返回此 Object 的运行时类。
package com.atguigu.polymorphism.grammar;
public class TestPolymorphism {
public static void main(String[] args) {
Pet p = new Dog();//p变量是Pet类型,但它指向Dog对象
System.out.println("p变量的运行时类型:" + p.getClass());
p = new Cat();//p变量还可以指向Cat对象。
System.out.println("p变量的运行时类型:" + p.getClass());
}
}
运行结果:
p变量的运行时类型:class com.atguigu.polymorphism.grammar.Dog
p变量的运行时类型:class com.atguigu.polymorphism.grammar.Cat
7.1.3 多态引用的应用场景
1、声明变量是父类类型,变量赋值子类对象
- 方法的形参是父类类型,调用方法的实参是子类对象
- 成员变量声明父类类型,实际存储的是子类对象
package com.atguigu.polymorphism.grammar;
public class Pet {
private String nickname;
public Pet(String nickname){
this.nickname = nickname;
}
public String getNickname() {
return nickname;
}
public void setNickname(String nickname) {
this.nickname = nickname;
}
public void eat(){
System.out.println("吃东西");
}
}
package com.atguigu.polymorphism.grammar;
public class Dog extends Pet {
public Dog(String nickname){
this.nickname = nickname;
}
public void watchHouse(){
System.out.println("看家");
}
@Override
public void eat(){
System.out.println("狗狗啃骨头");
}
@Override
public String toString() {
return "Dog{"+getNickname()+"}";
}
}
package com.atguigu.polymorphism.grammar;
public class Cat extends Pet{
public Cat(String nickname){
this.nickname = nickname;
}
public void catchMouse(){
System.out.println("抓老鼠");
}
@Override
public void eat(){
System.out.println("猫咪吃鱼仔");
}
@Override
public String toString() {
return "Cat{"+getNickname()+"}";
}
}
package com.atguigu.polymorphism.grammar;
public class Owner {
private String name;
private Pet pet;//成员变量声明为父类类型,实际赋值的是子类对象
public Owner(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Pet getPet() {
return pet;
}
public void setPet(Pet pet) {//方法形参声明为父类类型,实参是子类对象
this.pet = pet;
}
//feed:喂食
public void feed() {
if (pet!= null) {
//这里eat()执行哪个类的eat()方法,是根据pet对象的运行时类型动态绑定的
pet.eat();
}
}
@Override
public String toString() {
return "Owner{" +
"name='" + name + '\'' +
", pet=" + pet +
//这里+pet,会自动调用pet.toString(),至于执行哪个类的toString方法,也是根据pet对象的运行时类型动态绑定的
'}';
}
}
package com.atguigu.polymorphism.grammar;
public class TestPersonPet {
public static void main(String[] args) {
Owner zhang = new Owner("张三");
zhang.setPet(new Dog("旺财"));
zhang.feed();
System.out.println(zhang);
Owner li = new Owner("李四");
li.setPet(new Cat("雪球"));
li.feed();
System.out.println(li);
}
}
运行结果:
狗狗啃骨头
Owner{name='张三', pet=Dog{旺财}}
猫咪吃鱼仔
Owner{name='李四', pet=Cat{雪球}}
2、数组元素是父类类型,元素对象是子类对象
package com.atguigu.polymorphism.grammar;
public class Person {
private String name;
private Pet[] pets;
public Person(String name){
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Pet[] getPets() {
return pets;
}
public void setPets(Pet[] pets) {
this.pets = pets;
}
public void feed() {
for(int i=0; i
package com.atguigu.polymorphism.grammar;
public class TestPersonPets {
public static void main(String[] args) {
Person p = new Person("王五");
Pet[] pets = new Pet[2];
//数组的元素类型声明为父类类型,实际存储的是子类对象
pets[0] = new Dog("旺财");
pets[1] = new Cat("雪球");
p.setPets(pets);
p.feed();
System.out.println(p);
}
}
3、方法返回值类型声明为父类类型,实际返回的是子类对象
package com.atguigu.polymorphism.grammar;
public class PetShop {
//返回值类型是父类类型,实际返回的是子类对象
public Pet sale(String type){
switch (type){
case "Dog":
return new Dog();
case "Cat":
return new Cat();
}
return null;
}
}
package com.atguigu.polymorphism.grammar;
public class TestPetShop {
public static void main(String[] args) {
PetShop shop = new PetShop();
Pet p1 = shop.sale("Dog");
/*
p1的编译时类型是Pet,运行时类型是Dog
*/
p1.setNickname("小白");
System.out.println(p1);//自动调用Dog类的toString方法
Pet p2 = shop.sale("Cat");
/*
p2的编译时类型是Pet,运行时类型是Cat
*/
p2.setNickname("雪球");
System.out.println(p2);//自动调用Cat类的toString方法
}
}
7.1.4 向上转型与向下转型
1、向上转型
(1)什么是向上转型?
- 让一个子类对象在**在编译期间**,以父类的类型呈现,就是向上转型。
(2)如何实现向上转型呢?
- 当把子类对象赋值给父类的变量时,此时通过这个父类变量引用它时,这个子类对象就呈现为“父类的类型”,
Pet p = new Dog();
//接下来,通过p引用Dog对象,编译期间就呈现为Pet类型
(3)为什么要向上转型呢?
父类类型 > 子类类型。很多地方不得不使用父类类型代替子类类型声明变量。
2、向下转型
(1)什么是向下转型?
- 让一个父类的变量在**在编译期间**,以子类的类型呈现,就是向下转型。
(2)如何实现向下转型呢?
- 必须使用强制类型转换的语法
Pet p = new Dog();
Dog d = (Dog)p;
//接下来,通过d引用Dog对象,编译期间就呈现为Dog类型
(3)为什么要向下转型呢?
为了使用子类扩展的成员。
Pet p = new Dog();
Dog d = (Dog)p;
//接下来,通过d引用Dog对象,编译期间就呈现为Dog类型
d.watchHouse();
3、注意问题
(1)无论向上还是向下,都只发生在编译时,对象的运行时类型不会变
首先,一个对象在new的时候创建的是哪个类型的对象,它从头至尾都不会变。即这个对象的运行时类型,本质的类型不会变。但是,把这个对象赋值给不同类型的变量时,这些变量的编译时类型却不同。
这个和基本数据类型的转换是不同的。基本数据类型是把数据值copy了一份,相当于有两种数据类型的值。而对象的赋值不会产生两个对象。
(2)向上转型和向下转型只支持父子类或父接口与实现类之间。
Dog dog = "hello";//错误,不能向上也不能向下,不是父子类之间
(3)向上转型向下转型操作编译通过,运行时不一定通过
不是所有通过编译的转型转换都是正确的,可能会发生ClassCastException
package com.atguigu.polymorphism.grammar;
public class ClassCastTest {
public static void main(String[] args) {
//没有类型转换
Dog dog = new Dog();//dog的编译时类型和运行时类型都是Dog
//向上转型
Pet pet = new Dog("小白");//pet的编译时类型是Pet,运行时类型是Dog
pet.eat();//可以调用父类Pet有声明的方法eat,但执行的是子类重写的eat方法体
// pet.watchHouse();//不能调用父类没有的方法watchHouse
Dog d = (Dog) pet;
d.eat();//可以调用eat方法
d.watchHouse();//可以调用子类扩展的方法watchHouse
Cat c = (Cat) pet;//编译通过,因为从语法检查来说,pet的编译时类型是Pet,Cat是Pet的子类,所以向下转型语法正确
//这句代码运行报错ClassCastException,因为pet变量的运行时类型是Dog,Dog和Cat之间是没有继承关系的
}
}
4、instanceof关键字
为了避免ClassCastException的发生,Java提供了 `instanceof` 关键字,用来判断对象的类型关系,只要用instanceof判断返回true的,那么强转为该类型就一定是安全的,不会报ClassCastException异常。
变量 instanceof 数据类型
那么,哪些instanceof判断会返回true呢?
- 对象的编译时类型 与 instanceof后面数据类型是 父子类关系才编译通过
- 对象的运行时类型<= instanceof后面数据类型,运行结果才为true
package com.atguigu.polymorphism.grammar;
public class TestInstanceof {
public static void main(String[] args) {
Pet[] pets = new Pet[2];
pets[0] = new Dog("小白");//多态引用
pets[1] = new Cat("雪球");//多态引用
for (int i = 0; i < pets.length; i++) {
pets[i].eat();
if(pets[i] instanceof Dog){
Dog dog = (Dog) pets[i];
dog.watchHouse();
}else if(pets[i] instanceof Cat){
Cat cat = (Cat cat.catchMouse();
}
}
}
}
5、新特性:instanceof模式匹配
instanceof的模式匹配在JDK14、15中预览,在JDK16中转正。有了它就不需要编写先通过instanceof判断再强制转换的代码。
package com.atguigu.polymorphism.grammar;
public class TestInstanceof {
public static void main(String[] args) {
Pet[] pets = new Pet[2];
pets[0] = new Dog("小白");//多态引用
pets[1] = new Cat("雪球");//多态引用
for (int i = 0; i < pets.length; i++) {
pets[i].eat();
if(pets[i] instanceof Dog dog){//新特性
dog.watchHouse();
}else if(pets[i] instanceof Cat cat){//新特性
cat.catchMouse();
}
}
}
}
7.1.5 经典面试题
1、虚方法的静态分配和动态绑定
在Java中虚方法是指在编译阶段和类加载阶段都不能确定方法的调用入口地址,在运行阶段才能确定的方法,即可能被重写的方法。
当我们通过“对象.方法”的形式调用一个虚方法时,要如何确定它具体执行哪个方法呢?
==注意:super.方法 和 对象.非虚方法(静态方法,final修饰的方法等),不使用以下规则。==
(1)编译时静态分派:先看这个对象xx的编译时类型,在这个对象的编译时类型中找到能匹配的方法
匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度 A:找最匹配 实参的编译时类型 = 方法形参的类型 B:找兼容 实参的编译时类型 < 方法形参的类型
(2)运行时动态绑定:再看这个对象xx的运行时类型,如果这个对象xx的运行时类重写了刚刚找到的那个匹配的方法,那么执行重写的,否则仍然执行刚才编译时类型中的那个匹配的方法
package com.atguigu.polymorphism.virtual;
class MyClass{
public void method(Father f) {
System.out.println("father");
}
public void method(Son s) {
System.out.println("son");
}
}
class MySub extends MyClass{
public void method(Father d) {
System.out.println("sub--father");
}
public void method(Daughter d) {
System.out.println("daughter");
}
}
class Father{
}
class Son extends Father{
}
class Daughter extends Father{
}
package com.atguigu.polymorphism.virtual;
public class TestVirtualMethod {
public static void main(String[] args) {
Father f = new Father();
Son s = new Son();
Daughter d = new Daughter();
MyClass my = new MySub();
my.method(f);//sub--father
/*
(1)静态分派:看my的编译时类型MyClass,在MyClass中找最匹配的
匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配 实参的编译时类型 = 方法形参的类型
B:找兼容 实参的编译时类型 < 方法形参的类型
实参f的编译时类型是Father,形参(Father f) 、(Son s)
最匹配的是public void method(Father f)
(2)动态绑定:看my的运行时类型MySub,看在MySub中是否有对 public void method(Father f)进行重写
发现有重写,如果有重写,就执行重写的
public void method(Father d) {
System.out.println("sub--");
}
*/
my.method(s);//son
/*
(1)静态分派:看my的编译时类型MyClass,在MyClass中找最匹配的
匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配 实参的编译时类型 = 方法形参的类型
B:找兼容 实参的编译时类型 < 方法形参的类型
实参s的编译时类型是Son,形参(Father f) 、(Son s)
最匹配的是public void method(Son s)
(2)动态绑定:看my的运行时类型MySub,看在MySub中是否有对 public void method(Son s)进行重写
发现没有重写,如果没有重写,就执行刚刚父类中找到的方法
*/
my.method(d);//sub--father
/*
(1)静态分派:看my的编译时类型MyClass,在MyClass中找最匹配的
匹配的原则:看实参的编译时类型与方法形参的类型的匹配程度
A:找最匹配 实参的编译时类型 = 方法形参的类型
B:找兼容 实参的编译时类型 < 方法形参的类型
实参d的编译时类型是Daughter,形参(Father f) 、(Son s)
最匹配的是public void method(Father f)
(2)动态绑定:看my的运行时类型MySub,看在MySub中是否有对 public void method(Father f)进行重写
发现有重写,如果有重写,就执行重写的
public void method(Father d) {
System.out.println("sub--");
}
*/
}
}
2、成员变量没有多态一说
package com.atguigu.polymorphism.grammar;
public class Father {
int a = 1;
int c = 1;
}
package com.atguigu.polymorphism.grammar;
public class Son extends Father {
int a = 2;
int d = 2;
}
package com.atguigu.polymorphism.grammar;
public class TestField {
public static void main(String[] args) {
Father f = new Son();
System.out.println("f.a = " + f.a);
System.out.println("f.c = " + f.c);
// System.out.println("f.d" + f.d);//错误
System.out.println("((Son)f).a = " + ((Son)f).a );
System.out.println("((Son)f).d = " + ((Son)f).d);
System.out.println("-------------------");
Son s = new Son();
System.out.println("s.a = " + s.a);
System.out.println("s.c = " + s.c);
System.out.println("s.d = " + s.d);
System.out.println("((Father)s).a = " + ((Father)s).a);
System.out.println("((Father)s).c = " + ((Father)s).c);
// System.out.println("((Father)s).d = " + ((Father)s).d);//错误
}
}
7.2 内部类
7.2.1 概述
1、什么是内部类?
将一个类A定义在另一个类B里面,里面的那个类A就称为**内部类**,B则称为**外部类**。
2、为什么要声明内部类呢?
总的来说,遵循高内聚低耦合的面向对象开发总原则。便于代码维护和扩展。
3、内部类都有哪些形式?
根据内部类声明的位置(如同变量的分类),我们可以分为:
成员内部类:
- 静态成员内部类
- 非静态成员内部类
局部内部类:
- 有名字的局部内部类
- 匿名的内部类
7.2.2 成员内部类
如果成员内部类中不直接使用外部类的非静态成员,那么通常将内部类声明为静态内部类,否则声明为非静态内部类。
语法格式:
【修饰符】 class 外部类{
【其他修饰符】 【static】 class 内部类{
}
}
1、静态内部类
有static修饰的成员内部类叫做静态内部类。它的特点:
和其他类一样,它只是定义在外部类中的另一个完整的类结构
- 可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关
- 可以在静态内部类中声明属性、方法、构造器等结构,包括静态成员
- 编译后有自己的独立的字节码文件:外部类名$静态内部类名.class
- 在外部类的外面使用静态内部类名时需要使用”包名.外部类名.静态内部类名“
- 和外部类不同的是,它可以允许四种权限修饰符:public,protected,缺省,private
在外部类的任意位置都可以直接使用静态内部类,也可以访问静态内部类的所有成员,包括私有的
- 如果使用静态内部类的静态成员,就通过”静态内部类名.静态成员“的形式
- 如果使用静态内部类的非静态成员,就先创建静态内部类的对象,然后通过“静态内部类对象.非静态成员”的形式
==只可以==在静态内部类中使用外部类的静态成员
- 在静态内部类中不能直接使用外部类的非静态成员哦
- 如果在内部类中有变量与外部类的静态变量同名,可以使用“外部类名.静态变量"进行区别
如果权限修饰符允许,静态内部类也可以在外部类的外面使用。
- 如果使用静态内部类的静态成员,就通过”外部类名.静态内部类名.静态成员“的形式
- 如果使用静态内部类的非静态成员,就先创建静态内部类的对象,在外部类的外面不需要通过外部类的对象就可以创建静态内部类的对象
其实严格的讲(在James Gosling等人编著的《The Java Language Specification》书中)静态内部类不是内部类,而是类似于C++的嵌套类的概念,外部类仅仅是静态内部类的一种命名空间的限定名形式而已。所以接口中的内部类通常都不叫内部类,因为接口中的内部成员都是隐式是静态的(即public static)。例如:Map.Entry。
package com.atguigu.inner.staticinner;
public class TestStaticInner {
public static void main(String[] args) {
Outer.outMethod();
System.out.println("-----------------------");
Outer out = new Outer();
out.outFun();
System.out.println("####################################");
Outer.Inner.inMethod();
System.out.println("------------------------");
Outer.Inner inner = new Outer.Inner();
inner.inFun();
}
}
class Outer{
private static String a = "外部类的静态a";
private static String b = "外部类的静态b";
private String c = "外部类对象的非静态c";
private String d = "外部类对象的非静态d";
static class Inner{
private static String a ="静态内部类的静态a";
private String c = "静态内部类对象的非静态c";
public static void inMethod(){
System.out.println("Inner.inMethod");
System.out.println("Outer.a = " + Outer.a);
System.out.println("Outer.b = " + b);
// System.out.println("Outer对象.c = " + Outer.c + "," + Outer.this.c);//不能访问外部类的非静态成员
// System.out.println("Outer对象.d = " + Outer.d + "," + Outer.this.d);//不能访问外部类的非静态成员
System.out.println("Inner.a = " + a);
//System.out.println("Inner对象.c = " + c);//不能访问自己的非静态成员
}
public void inFun(){
System.out.println("Inner对象.inFun");
System.out.println("Outer.a = " + Outer.a);
System.out.println("Outer.b = " + b);
// System.out.println("Outer对象.c = " + Outer.c + "," + Outer.this.c);//不能访问外部类的非静态成员
// System.out.println("Outer对象.d = " + Outer.d + "," + Outer.this.d);//不能访问外部类的非静态成员
System.out.println("Inner.a = " + a);
System.out.println("Inner对象c = " + c);
}
}
public static void outMethod(){
System.out.println("Outer.outMethod");
System.out.println("Outer.a = " + a);
System.out.println("Outer.b = " + b);
// System.out.println("Outer对象.c = " + c);
// System.out.println("Outer对象.d = " + d);
System.out.println("Inner.a = " + Inner.a);
Inner in = new Inner();
System.out.println("Inner对象.c = " + in.c);
}
public void outFun(){
System.out.println("Outer对象.outFun");
System.out.println("Outer.a = " + a);
System.out.println("Outer.b = " + b);
System.out.println("Outer对象.c = " + c);
System.out.println("Outer.objects.d = " + d);
System.out.println("Inner.a = " + Inner.a);
Inner in = new Inner();
System.out.println("Inner.objects.c = " + in.c);
}
}
2、非静态成员内部类
没有static修饰的成员内部类叫做非静态内部类。非静态内部类的特点:
和其他类一样,它只是定义在外部类中的另一个完整的类结构
- 可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关
- 可以在非静态内部类中声明属性、方法、构造器等结构,JDK16之后**也允许声明静态成员**。
- 编译后有自己的独立的字节码文件:外部类名$非静态内部类名.class
- 在外部类的外面使用非静态内部类名时需要使用”包名.外部类名.非静态内部类名“
- 和外部类不同的是,它可以允许四种权限修饰符:public,protected,缺省,private
在外部类中使用非静态内部类==有限制==
- 在外部类的静态成员中,只能使用非静态内部类的静态常量
- 在外部类的非静态成员中,可以创建非静态内部类对象,然后通过非静态内部类的对象使用非静态内部类的所有成员
可以在非静态内部类中使用外部类的**所有成员**,哪怕是私有的
- 如果没有重名问题,可以直接使用外部类的所有成员
- 如果非静态内部类与外部类的静态成员重名,可以使用“外部类名."进行区别
- 如果非静态内部类与外部类的非静态成员重名,可以使用“外部类名.this."进行区别
如果权限修饰符允许,非静态内部类也可以在外部类的外面使用。
- 如果使用非静态内部类的静态成员,就通过”外部类名.非静态内部类名.静态成员“的形式
在外部类的外面必须通过外部类的对象才能创建非静态内部类的对象(通常应该避免这样使用)
- 如果要在外部类的外面使用非静态内部类的对象,通常在外部类中提供一个方法来返回这个非静态内部类的对象比较合适
- 因此在非静态内部类的方法中有两个this对象,一个是外部类的this对象,一个是内部类的this对象
package com.atguigu.inner.notstaticinner;
public class TestNonStaticInner {
public static void main(String[] args) {
Outer.outMethod();
System.out.println("-----------------------");
Outer out = new Outer();
out.outFun();
System.out.println("####################################");
System.out.println(Outer.Inner.s);
System.out.println("-----------------------");
Outer.Inner inner = new Outer().new Inner();
inner.inFun();
}
}
class Outer{
private static String a = "外部类的静态a";
private static String b = "外部类的静态b";
private String c = "外部类对象的非静态c";
private String d = "外部类对象的非静态d";
class Inner{
private String a = "非静态内部类对象的非静态a";
private String c = "非静态内部类对象的非静态c";
public static final String s = "非静态内部类的静态常量";
public void inFun(){
System.out.println("Inner对象.inFun");
System.out.println("Outer.a = " + Outer.a);
System.out.println("Outer.b = " + b);
System.out.println("Outer对象.c = " + Outer.this.c);
System.out.println("Outer.objects.d = " + d);
System.out.println("Inner对象.a = " + a);
System.out.println("Inner对象c = " + c);
System.out.println("Inner.s = " + Inner.s);
}
}
public static void outMethod(){
System.out.println("Outer.outMethod");
System.out.println("Outer.a = " + a);
System.out.println("Outer.b = " + b);
// System.out.println("Outer对象.c = " + c);
// System.out.println("Outer.objects.d = " + d);
// Inner in = new Inner();//此处无法使用非静态内部类
System.out.println("Inner.s = " + Inner.s);
}
public void outFun(){
System.out.println("Outer.objects.outFun");
System.out.println("Outer.a = " + a);
System.out.println("Outer.b = " + b);
System.out.println("Outer.objects.c = " + c);
System.out.println("Outer.objects.d = " + d);
Inner in = new Inner();
System.out.println("Inner.objects.a = " + in.a);
System.out.println("Inner.objects.c = " + in.c);
System.out.println("Inner.s = " + Inner.s);
}
}
7.2.3 局部内部类
1、局部内部类
语法格式:
【修饰符】 class 外部类{
【修饰符】 返回值类型 方法名(【形参列表】){
【final/abstract】 class 内部类{
}
}
}
局部内部类的特点:
和外部类一样,它只是定义在外部类的某个方法中的另一个完整的类结构
- 可以继承自己的想要继承的父类,实现自己想要实现的父接口们,和外部类的父类和父接口无关
- 可以在局部内部类中声明属性、方法、构造器等结构,JDK16之后**也允许声明静态成员**
- 编译后有自己的独立的字节码文件:外部类名$数字编号局部内部类名.class。
- 这里有编号是因为同一个外部类中,不同的方法中存在相同名称的局部内部类
- 和成员内部类不同的是,它前面不能有权限修饰符等
- 局部内部类如同局部变量一样,有作用域
- 局部内部类中是否能访问外部类的非静态的成员,取决于所在的方法
局部内部类中还可以使用所在方法的局部常量,即用final声明的局部变量
- JDK1.8之后,如果某个局部变量在局部内部类中被使用了,自动加final
- 为什么在局部内部类中使用外部类方法的局部变量要加final呢?考虑生命周期问题。
package com.atguigu.inner.local;
public class TestLocalInner {
public static void main(String[] args) {
Outer.outMethod();
System.out.println("-------------------");
Outer out = new Outer();
out.outFun();
System.out.println("===========================");
//这里不能使用局部内部类
// Outer.Inner in;
//但是这里可以获取局部内部类的对象,只能通过多态引用该局部内部类的对象
Father f = Outer.outTest();
f.m();
}
}
class Outer{
private static String a = "外部类的静态a";
private static String b = "外部类的静态b";
private String c = "外部类对象的非静态c";
private String d = "外部类对象的非静态d";
public static void outMethod(){
class Inner{
private String a = "非静态内部类1对象的非静态a";
private String c = "非静态内部类1对象的非静态c";
public static final String s = "非静态内部类1的静态常量";
public void inFun(){
System.out.println("Inner对象.inFun");
System.out.println("Outer.a = " + Outer.a);
System.out.println("Outer.b = " + b);
// System.out.println("Outer对象.c = " + Outer.this.c);
// System.out.println("Outer.objects.d = " + d);
System.out.println("Inner1对象.a = " + a);
System.out.println("Inner1对象c = " + c);
System.out.println("Inner1.s = " + Inner.s);
}
}
new Inner().inFun();
}
public void outFun(){
class Inner{
private String a = "非静态内部类2对象的非静态a";
private String c = "非静态内部类2对象的非静态c";
public static final String s = "非静态内部类2的静态常量";
public void inFun(){
System.out.println("Inner2对象.inFun");
System.out.println("Outer.a = " + Outer.a);
System.out.println("Outer.b = " + b);
System.out.println("Outer.objects.c = " + Outer.this.c);
System.out.println("Outer.objects.d = " + d);
System.out.println("Inner2对象.a = " + a);
System.out.println("Inner2对象c = " + c);
System.out.println("Inner2.s = " + Inner.s);
}
}
new Inner().inFun();
}
public static Father outTest(){
final int outA = 1;
class Inner extends Father{
@Override
void m() {
System.out.println("局部内部类重写父类方法");
System.out.println("局部内部类使用外部类的局部变量outA = " + outA);
}
}
return new Inner();
}
public static void outOtherMethod(){
// Inner in = new Inner();//局部内部类有作用域范围限制
}
}
abstract class Father{
abstract void m();
}
package com.atguigu.inner.local;
public class OuterClass {
public static void outTest(final int value){
final int outA = 1;
class InnerClass{
void m() {
System.out.println("value = " + value);
System.out.println("outA = " + outA);
}
}
}
}
如果外部类局部变量的值是确定的常量值,那么编译器直接在局部内部类中就相当于使用了一个确定常量值;
如果外部类局部变量的值是待确定的常量值,那么编译器会在局部内部类中用一个隐式的成员变量接收该局部变量的值,当局部内部类对象创建时自动完成赋值,即局部内部类中使用的已经不再是外部类的局部变量了,而是自己的一个成员变量;
package com.atguigu.inner.local;
public class OuterClass {
public static Object outTest(final int value){
final int outA = 1;
class InnerClass{
void m() {
System.out.println("value = " + value);
System.out.println("outA = " + outA);
}
@Override
public String toString() {
return "value = " + value + ",outA = " + outA;
}
}
// value = 2;
// outA = 2;
return new InnerClass();
}
public static void main(String[] args) {
System.out.println(outTest(1));
}
}
2、匿名内部类
当我们在开发过程中,需要用到一个抽象类的子类的对象或一个接口的实现类的对象,而且只创建一个对象,而且逻辑代码也不复杂。那么我们原先怎么做的呢?
- (1)编写类,继承这个父类或实现这个接口
- (2)重写父类或父接口的方法
- (3)创建这个子类或实现类的对象
这里,因为考虑到这个子类或实现类是一次性的,那么我们“费尽心机”的给它取名字,就显得多余。那么我们完全可以使用匿名内部类的方式来实现,避免给类命名的问题。
new 父类(【实参列表】){
重写方法...
}
//()中是否需要【实参列表】,看你想要让这个匿名内部类调用父类的哪个构造器,如果调用父类的无参构造,那么()中就不用写参数,如果调用父类的有参构造,那么()中需要传入实参
new 父接口(){
重写方法...
}
//()中没有参数,因为此时匿名内部类的父类是Object类,它只有一个无参构造
注意:
匿名内部类没有名字,因此字节码文件名是外部类名$编号.class。
匿名内部类是一种特殊的局部内部类,只不过没有名称而已。所有局部内部类的限制都适用于匿名内部类。例如:
- 在匿名内部类中是否可以使用外部类的非静态成员变量,看所在方法是否静态
- 在匿名内部类中如果需要访问当前方法的局部变量,该局部变量需要加final
思考:这个对象能做什么呢?
(1)使用匿名内部类的对象直接调用方法
interface A{
void a();
}
public class Test{
public static void main(String[] args){
new A(){
@Override
public void a() {
System.out.println("aaaa");
}
}.a();
}
}
(2)通过父类或父接口的变量多态引用匿名内部类的对象
interface A{
void a();
}
public class Test{
public static void main(String[] args){
A obj = new A(){
@Override
public void a() {
System.out.println("aaaa");
}
};
obj.a();
}
}
(3)匿名内部类的对象作为实参
interface A{
void method();
}
public class Test{
public static void test(A a){
a.method();
}
public static void main(String[] args){
test(new A(){
@Override
public void method() {
System.out.println("aaaa");
}
});
}
}
7.2.4 几种内部类对比
静态内部类 | 非静态内部类 | 局部内部类 | 匿名内部类 | ||
---|---|---|---|---|---|
类角色 | 字节码文件 | 外部类名$静态内部类名.class | 外部类名$非静态内部类名.class | 外部类名$编号局部内部类名.class | 外部类名$编号.class |
父类或父接口 | 正常 | 正常 | 正常 | 指定一个直接父类或一个直接父接口 | |
类成员 | 正常 | 正常 | 正常 | 正常,构造器只能有默认构造器 | |
权限修饰符 | public、protected、缺省、private | public、protected、缺省、private | 无 | 无 | |
成员角色 | 依赖于外部类 | 依赖 | 依赖 | 依赖 | 依赖 |
依赖于外部类的对象 | 不依赖 | 依赖 | 看所在方法 | 看所在方法 | |
在外部类的静态成员中使用内部类 | 没有限制 | 不能直接new非静态内部类的对象 | 有严格作用域 | 有严格作用域 | |
在外部类的非静态成员中使用内部类 | 没有限制 | 没有限制 | 有严格作用域 | 有严格作用域 | |
在内部类中使用外部类的静态成员 | 没有限制 | 没有限制 | 没有限制 | 没有限制 | |
成员角色(续) | 在内部类中使用外部类的非静态成员 | 不能 | 没有限制 | 看所在方法 | 看所在方法 |
在内部类中使用外部类的某局部变量 | 无 | 无 | 局部变量加final | 局部变量加final | |
在外部类的外面使用内部类的静态成员 | 外部类名.静态内部类名.静态成员 | 外部类名.非静态内部类名.静态成员 | 无 | 无 | |
在外部类的外面获取内部类的对象 | 外部类名.静态内部类名 变量 = 外部类名.静态内部类名(); | 外部类名 out变量 = new 外部类(); 外部类名.非静态内部类名 in变量 = out变量.new 非静态内部类名(); | 只能通过外部类的某方法返回局部内部类的对象; | 只能通过外部类的某方法返回匿名内部类的对象; | |
与外部类的静态成员重名 | 外部类名.静态成员 | 外部类名.静态成员 | 外部类名.静态成员 | 外部类名.静态成员 | |
与外部类的非静态成员重名 | 无 | 外部类名.this.非静态成员 | 同左 | 同左 | |
选择依据 | 在内部类中不使用外部类的非静态成员 | 在内部类中需要使用外部类的非静态成员。 | 该内部类仅限于当前方法使用 | 该内部类代码简洁,对象唯一性 |
7.3 枚举
7.3.1 概述
某些类型的对象是有限的几个,这样的例子举不胜举:
- 星期:Monday(星期一)......Sunday(星期天)
- 性别:Man(男)、Woman(女)
- 月份:January(1月)......December(12月)
- 季节:Spring(春节)......Winter(冬天)
- 支付方式:Cash(现金)、WeChatPay(微信)、Alipay(支付宝)、BankCard(银行卡)、CreditCard(信用卡)
- 员工工作状态:Busy(忙)、Free(闲)、Vocation(休假)
- 订单状态:Nonpayment(未付款)、Paid(已付款)、Fulfilled(已配货)、Delivered(已发货)、Checked(已确认收货)、Return(退货)、Exchange(换货)、Cancel(取消)
枚举类型本质上也是一种类,只不过是这个类的对象是固定的几个,而不能随意让用户创建。
在JDK1.5之前,需要程序员自己通过特殊的方式来定义枚举类型。
在JDK1.5之后,Java支持enum关键字来快速的定义枚举类型。
7.3.2 JDK1.5之前
在JDK1.5之前如何声明枚举类呢?
- 构造器加private私有化
- 本类内部创建一组常量对象,并添加public static修饰符,对外暴露这些常量对象
public class Season{
public static final Season SPRING = new Season();
public static final Season SUMMER = new Season();
public static final Season AUTUMN = new Season();
public static final Season WINTER = new Season();
private Season(){
}
public String toString(){
if(this == SPRING){
return "春";
}else if(this == SUMMER){
return "夏";
}else if(this == AUTUMN){
return "秋";
}else{
return "冬";
}
}
}
public class TestSeason {
public static void main(String[] args) {
Season spring = Season.SPRING;
System.out.println(spring);
}
}
7.3.3 JDK1.5之后
1、enum关键字声明枚举
语法格式:
【修饰符】 enum 枚举类名{
常量对象列表
}
【修饰符】 enum 枚举类名{
常量对象列表;
其他成员列表;
}
package com.atguigu.enumeration;
public enum Week {
MONDAY,TUESDAY,WEDNESDAY,THURSDAY,FRIDAY,SATURDAY,SUNDAY
}
public class TestEnum {
public static void main(String[] args) {
Season spring = Season.SPRING;
System.out.println(spring);
}
}
2、枚举类的要求和特点
枚举类的要求和特点:
- 枚举类的常量对象列表必须在枚举类的首行,因为是常量,所以建议大写。
- 如果常量对象列表后面没有其他代码,那么“;”可以省略,否则不可以省略“;”。
- 编译器给枚举类默认提供的是private的无参构造,如果枚举类需要的是无参构造,就不需要声明,写常量对象列表时也不用加参数,
- 如果枚举类需要的是有参构造,需要手动定义,有参构造的private可以省略,调用有参构造的方法就是在常量对象名后面加(实参列表)就可以。
- 枚举类默认继承的是java.lang.Enum类,因此不能再继承其他的类型。
- JDK1.5之后switch,提供支持枚举类型,case后面可以写枚举常量名。
- 枚举类型如有其它属性,建议(**不是必须**)这些属性也声明为final的,因为常量对象在逻辑意义上应该不可变。
package com.atguigu.enumeration;
public enum Week {
MONDAY("星期一"),
TUESDAY("星期二"),
WEDNESDAY("星期三"),
THURSDAY("星期四"),
FRIDAY("星期五"),
SATURDAY("星期六"),
SUNDAY("星期日");
private final String description;
private Week(String description){
this.description = description;
}
@Override
public String toString() {
return super.toString() +":"+ description;
}
}
package com.atguigu.enumeration;
public class TestWeek {
public static void main(String[] args) {
Week week = Week.MONDAY;
System.out.println(week);
switch (week){
case MONDAY:
System.out.println("怀念周末,困意很浓");break;
case TUESDAY:
System.out.println("进入学习状态");break;
case WEDNESDAY:
System.out.println("死撑");break;
case THURSDAY:
System.out.println("小放松");break;
case FRIDAY:
System.out.println("又信心满满");break;
case SATURDAY:
System.out.println("开始盼周末,无心学习");break;
case SUNDAY:
System.out.println("一觉到下午");break;
}
}
}
3、枚举类型常用方法
常用方法如下:
String toString()
: 默认返回的是常量名(对象名),可以继续手动重写该方法!String name()
:返回的是常量名(对象名)int ordinal()
:返回常量的次序号,默认从0开始枚举类型[] values()
:返回该枚举类的所有的常量对象,返回类型是当前枚举的数组类型,是一个静态方法枚举类型 valueOf(String name)
:根据枚举常量对象名称获取枚举对象
package com.atguigu.enumeration;
import java.util.Scanner;
public class TestEnumMethod {
public static void main(String[] args) {
Week[] values = Week.values();
for (int i = 0; i < values.length; i++) {
System.out.println((values[i].ordinal()+1) + "->" + values[i].name());
}
System.out.println("------------------------");
Scanner input = new Scanner(System.in);
System.out.print("请输入星期值:");
int weekValue = input.nextInt();
Week week = values[weekValue-1];
System.out.println(week);
System.out.print("请输入星期名:");
String weekName =曦input.next();
week = Week.valueOf(weekName);
System.out.println(week);
input.close();
}
}
7.4 新特性:记录类(了解)
Record类在JDK14、15预览特性,在JDK16中转正。
record是一种全新的类型,它本质上是一个 final类,同时所有的属性都是 final修饰,它会自动编译出get、hashCode 、比较所有属性值的equals、toString 等方法,减少了代码编写量。使用 Record 可以更方便的创建一个常量类。
1.注意:
- Record只会有一个全参构造
- 重写的equals方法比较所有属性值
- 可以在Record声明的类中定义静态字段、静态方法或实例方法。
- 不能在Record声明的类中定义实例字段;
- 不能显式的声明父类,默认父类是java.lang.Record类
- 因为Record类是一个 final类,所以也没有子类等。因此rRecord类不能声明为abstract;
- 属性也是final
package com.atguigu.record;
public class TestRecord {
public static void main(String[] args) {
Triangle t = new Triangle(3, 4, 5);
System.out.println(t);
System.out.println("面积:" + t.area());
System.out.println("周长:" + t.perimeter());
System.out.println("边长:" + t.a() + "," + t.b() + "," + t.c());
Triangle t2 = new Triangle(3, 4, 5);
System.out.println(t.equals(t2));
}
}
record Triangle(double a, double b, double c) {
public double area() {
if (a > 0 && b > 0 && c > 0 && a + b > c && b + c > a && a + c > b) {
double p = (a + b + c) / 2;
return Math.sqrt(p * (p - a) * (p - b) * (p - c));
}
return 0.0;
}
public double perimeter() {
if (a > 0 && b > 0 && c > 0 && a + b > c && b + c > a && a + c > b) {
return a + b + c;
}
return 0.0;
}
}
7.5 新特性:密封类(了解)
其实很多语言中都有`密封类`的概念,在Java语言中,也早就有`密封类`的思想,就是final修饰的类,该类不允许被继承。而从JDK15开始,针对`密封类`进行了升级。
Java 15通过密封的类和接口来增强Java编程语言,这是新引入的预览功能并在Java 16中进行了二次预览,并在Java17最终确定下来。这个预览功能用于限制超类的使用,密封的类和接口限制其他可能继承或实现它们的其他类或接口。
语法格式如下:
【修饰剂】 sealed class 密封类 【extends 父类】【implements 父接口】 permits 子类{
}
【修饰剂】 sealed interface 接口 【extends 父接口们】 permits 实现类{
}
相关特点如下:
- 密封类用 sealed 修饰符来描述,
- 使用 permits 关键字来指定可以继承或实现该类的类型有哪些
- 一个类继承密封类或实现密封接口,该类必须是sealed、non-sealed、final修饰的。
- sealed修饰的类或接口必须有子类或实现类
package com.atguigu.sealed;
sealed class Graphic permits Circle,Rectangle, Triangle {
}
final class Triangle extends Graphic{
}
non-sealed class Circle extends Graphic{
}
sealed class Rectangle extends Graphic permits Square{
}
final class Square extends Rectangle{
}
7.6 代码块(了解)
代码块分为静态代码块和非静态代码块。
7.6.1 静态代码块与类初始化
静态代码块又称为类初始化块,用于在类初始化时给静态变量初始化。
执行特点:每一个类的静态代码块只会执行1次,并且在类被加载时执行。
语法格式:
【修饰符】 class 类{
static{
//静态代码块
}
}
7.6.2 非静态代码块与实例初始化
非静态代码块又称为实例初始化块,或者构造块,用于在创建对象时给实例变量初始化。
执行特点:每次new对象时,并且一定是先于本类构造器除了super()或super(实参列表)以外的所有代码执行。
语法格式:
【修饰符】 class 类{
{
//非静态代码块
}
}