【Java难点】内部类

一、初识内部类

1.什么是内部类?

在一个类里面再定义一个其他类,这个其他类就称为内部类。

类的五大成员:属性、方法、构造方法、代码块、内部类。

例:

public class Outer {
    public class Inner{}
}
class Other{}

Outer是外部类,Inner是内部类,Other是外部其他类

2.为什么要学习内部类?
2.1 内部类的特点
  • 内部类表示的事物是外部类的一部分

  • 内部类单独出现没有任何意义

2.2 内部类的访问特点
  • 内部类可以直接访问外部类的成员,包括私有

  • 外部类要访问内部类的成员,必须创建内部类对象

例:

写一个javabean类描述汽车。要求汽车具有以下属性:汽车的品牌,车龄,颜色,发动机的品牌,发动机的使用年限。

public class Car {
    String carName;
    private int carAge;
    int carColor;
    public void show(){
        System.out.println(carName);
//        System.out.println(engineName); //编译报错,访问内部类的成员变量,需要先创建内部类
        Engine engine = new Engine();
        System.out.println(engine.engineName);
    }
    class Engine {
        String engineName;
        int engineAge;
        public void show(){
            System.out.println(engineName); //直接访问内部类的成员变量
            System.out.println(carName); //直接访问外部类的成员变量
            System.out.println(carAge); //直接访问外部类的私有成员变量
        }
    }
}
3.什么时候用到内部类?

B类表示的事物是A类的一部分,且B单独存在没有意义,则可以将B类定义为A类的内部类。

比如:汽车的发动机,ArrayList的迭代器,人的心脏等。

4.内部类的分类
  1. 成员内部内,类定义在了成员位置 (类中方法外称为成员位置,无static修饰的内部类)
  2. 静态内部类,类定义在了成员位置 (类中方法外称为成员位置,有static修饰的内部类)
  3. 局部内部类,类定义在方法内
  4. 匿名内部类,没有名字的内部类,可以在方法中,也可以在类中方法外。

二、成员内部类

1.成员内部类特点
  • 无static修饰的内部类,属于外部类对象的。
  • 宿主:外部类对象。
//Car是外部类,Engine是成员内部类。
public class Car{
  String carName;
  int carAge;
  int carColor;
  class Engine{
    String engineName;
    int engineAge;
  }
}
2.成员内部类的使用格式
外部类.内部类。 // 访问内部类的类型都是用 外部类.内部类
获取成员内部类对象的两种方式
  1. 外部直接创建成员内部类的对象
外部类名.内部类名 变量 = new 外部类名().new 内部类名();
  1. 在外部类中定义一个方法提供内部类的对象

案例演示

方式一:
public class Test {
    public static void main(String[] args) {
        //  宿主:外部类对象。
       // Outer out = new Outer();
        // 创建内部类对象。
        Outer.Inner oi = new Outer().new Inner();
        oi.method();
    }
}
class Outer {
    // 成员内部类,属于外部类对象的。
    public class Inner{
        // 这里面的东西与类是完全一样的。
        public void method(){
            System.out.println("内部类中的方法被调用了");
        }
    }
}

方式二:
public class Outer {
    String name;
    private class Inner{
        static int a = 10;
    }
    public Inner getInstance(){
        return new Inner();
    }
}
public class Test {
    public static void main(String[] args) {
        Outer o = new Outer();
        System.out.println(o.getInstance()); 
//打印结果:com.itheima.a02innerclassdemo2.Outer$Inner@4eec7777,Outer$Inner是内部类的字节码文件名,$前面是外部类名,后面是内部类名。4eec7777是内部类对象的内存地址。
    }
}
4.成员内部类的细节
  • 成员内部类可以被一些修饰符所修饰,比如private、默认(不写)、protected、public等。内部类被private修饰,外界无法直接获取内部类的对象,只能通过案例演示中的方式二获取内部类的对象,被其他权限修饰符修饰的内部类一般用案例演示中的方式一直接获取内部类的对象。

  • 在成员内部类里面,JDK16之前不能定义静态变量,JDK16开始才可以定义静态变量。

  • 创建成员内部类对象时,成员内部类对象对象中有一个隐含的Outer.this记录外部类对象的地址值。(请参见下一节的内存图)

  • 内部类如果想要访问外部类的成员变量,外部类的变量必须用final修饰,JDK8以前必须手动写final,JDK8及以后版本不需要手动写,JDK默认加上。

5.成员内部类在JDK中的应用
public class ArrayList<E> extends AbstractList<E> implements List<E>, RandomAccess, Cloneable, java.io.Serializable{
 	...
  public Iterator<E> iterator() {
    return new Itr();
  }

  private class Itr implements Iterator<E> {
    int cursor;
    ...
  }
  ...
}

ArrayList类中定义了成员内部类Itr,该类被private修饰,外界无法创建Itr对象,所以ArrayList类中提供了iterator方法来获取成员内部类Itr。

6.内部类访问外部类对象
  • 如果变量的名字不相同,则会自动在本类中寻找对应的变量。
public class Outer {
    private int a=10;
    class Inner{
        private int b=20;
        public void show(){
            int c=30;
            System.out.println(a);
            System.out.println(b);
            System.out.println(c);
        }
    }
}
public class MyTest {
    public static void main(String[] args){
        Outer.Inner oi = new Outer().new Inner();
        oi.show();
    }
}
//输出:
//10
//20
//30
  • 如果变量的名字相同,则需要指定变量。内部类访问外部类对象的格式是:外部类名.this
public class Outer {
    private int a=10;
    class Inner{
        private int a=20;
        public void show(){
            int a=30;
          //Outer.this表示获取了外部类对象的地址
            System.out.println(Outer.this.a); //Outer.this.a:获取外部类Outer对象的成员变量a,打印10
            System.out.println(this.a); //this.a:获取当前对象的成员变量a,打印20
            System.out.println(a);//就近原则,打印30
        }
    }
}
public class MyTest {
    public static void main(String[] args){
        Outer.Inner oi = new Outer().new Inner();
        oi.show();
    }
}
//输出:
//10
//20
//30
7.成员内部类内存图
  • 内部类和外部类是两个独立的字节码文件

image-20240104221119928

  • 内存图

内部类内存图

  • 使用jhsdb验证
  1. 运行程序,使用new Scanner(System.in)来保证程序处于一直运行状态

image-20240104223247243

  1. 使用jps来查看JVM中Test进程的id

image-20240104223431101

  1. 使用jhsdb hsdb命令来启动内存分析工具,然后输入Test进程的id

image-20240104223605085

  1. 查看字节码文件中内容,点击Tools->Class Browser

image-20240104224345799

image-20240104224634678

三、静态内部类

1.静态内部类特点
  • 静态内部类是一种由static修饰的特殊成员内部类。
  • 静态内部类只能访问外部类中的静态变量和静态方法。
  • 静态内部类不可以直接访问外部类的非静态成员,如果要访问需要创建外部类的对象。
  • 静态内部类中没有外部类名.this
2.静态内部类的使用格式

静态内部类与其他类的用法完全一样。只是访问的时候需要加上外部类名.内部类名

3.静态内部类对象的创建格式
外部类名.内部类名  变量 = new 外部类名.内部类名();
4.调用静态内部类方法的格式
  • 调用非静态方法的格式:先创建对象,用对象调用
  • 调用静态方法的格式:外部类名.内部类名.方法名()

案例演示:

class Outer01{
    private static  String sc_name = "黑马程序";
  	private int a = 100;
    public static class Inner01{
      	public static int age=200;
        private String name;
        public Inner01(String name) {
            this.name = name;
        }
        public void showName(){
            System.out.println(this.name);
            //静态内部类可以直接访问外部类的静态成员。
            System.out.println(sc_name);
          	//静态内部类访问外部类的非静态成员,需要先创建外部类对象
          	Outer01 o = new Outer01();
          	System.out.println(o.a);
        }
    }
}

public class InnerClassDemo01 {
    public static void main(String[] args) {
        // 创建静态内部类对象。
        // 外部类名.内部类名 变量 = new 外部类名.内部类名();
        Outer01.Inner01 in  = new Outer01.Inner01("张三");
        in.showName();
        //使用静态内部类中的静态属性
        System.out.println(Outer01.Inner01.age);
    }
}

//输出:
//张三
//黑马程序
//100
//200

四、局部内部类

1.局部内部类特点
  • 将内部类定义在方法里面就叫做局部内部类,类似于方法里面的局部变量。

  • 外界无法直接使用局部内部类,需要在方法内部创建局部内部类对象并使用。

  • 局部内部类可以直接访问外部类的成员,也可以访问方法内的局部变量。

  • 可以修饰局部变量的关键字同样可以修饰局部内部类,如final

  • 不可以修饰局部变量的关键字同样不可以修饰局部内部类,如public、默认(不写)、protected、private等

  • JDK16之前不能在局部内部类中定义静态变量和静态方法,JDK16开始才可以。

  • 创建局部内部类对象时,局部内部类对象中有一个隐含的Outer.this记录外部类对象的地址值

案例演示:

public class Outer {
    int a=20;
    int b=30;
    public void show() {
        int a = 10;
//        定义局部内部类
        class Inner{
            String name;
            int age;
            public void method1(){
                System.out.println(a); //打印局部内部类的成员变量a
                System.out.println(Outer.this.a); //打印外部类的成员变量a
                System.out.println(b); //打印外部类的成员变量b
                System.out.println("局部内部类中的method1方法");
            }
        }
        Inner inner = new Inner();
        inner.method1();
    }
}
public class MyTest {
    public static void main(String[] args){
        Outer outer = new Outer();
        outer.show();
    }
}
//输出:
//10
//20
//30
//局部内部类中的method1方法

五、匿名内部类

匿名内部类本质上就是隐藏了名字的内部类。可以写在成员位置,也可以写在局部位置。

1.匿名内部类的格式
new 类名或者接口名(){重写方法};

案例演示:

  • new 接口名
public interface Swim {
    void swim();
}
public class MyTest {
    public static void main(String[] args) {
//        编写匿名内部类代码
        new Swim() {
            @Override
            public void swim() {
                System.out.println("重写了游泳的方法");
            }
        };
    }
}
  • new 类名
public abstract class Animal {
    public abstract void eat();
}
public class MyTest {
    public static void main(String[] args) {
//        编写匿名内部类代码
        new Animal(){
            @Override
            public void eat() {
                System.out.println("重写了吃方法");
            }
        };
    }
}

案例解析:

{
  @Override
  public void swim() {
    System.out.println("重写了游泳的方法");
  }
};表示没有名字的类,该类实现了Swim接口
{
  @Override
  public void eat() {
    System.out.println("重写了吃方法");
  }
};表示没有名字的类,该类继承了了Animal

匿名内部类的格式包含了

  • 继承或者实现关系

  • 方法重写

  • 创建对象

所以从语法上来讲,这个整体其实是一个类的子类对象或者接口的实现类对象。

2.解析匿名内部类
  1. 编译以下代码:
public interface Swim {
    void swim();
}
public abstract class Animal {
    public abstract void eat();
}
public class MyTest {
    public static void main(String[] args) {
        //        编写匿名内部类代码
        new Swim() {
            @Override
            public void swim() {
                System.out.println("重写了游泳的方法");
            }
        };
//        编写匿名内部类代码
        new Animal(){
            @Override
            public void eat() {
                System.out.println("重写了吃方法");
            }
        };
    }
}
  1. 会生成下面的字节码文件

image-20240105003213690

  1. 对MyTest$1.class进行反编译

image-20240105003916998

  1. 对MyTest$2.class进行反编译

image-20240105004022356

3.什么时候用到匿名内部类

如果我们希望定义一个只要使用一次的类,就可考虑使用匿名内部类。匿名内部类的本质作用是为了简化代码

之前我们使用接口时,似乎得做如下几步操作:

  1. 定义子类
  2. 重写接口中的方法
  3. 创建子类对象
  4. 调用重写后的方法
interface Swim {
    public abstract void swimming();
}

// 1. 定义接口的实现类
class Student implements Swim {
    // 2. 重写抽象方法
    @Override
    public void swimming() {
        System.out.println("狗刨式...");
    }
}

public class Test {
    public static void main(String[] args) {
        // 3. 创建实现类对象
        Student s = new Student();
        // 4. 调用方法
        s.swimming();
    }
}

我们的目的,最终只是为了调用方法,那么能不能简化一下,把以上四步合成一步呢?匿名内部类就是做这样的快捷方式。

4.匿名内部类前提

匿名内部类必须继承一个父类或者实现一个父接口

5.其他使用方式
public interface Swim {
    void swimming();
}
public class MyTest {
    public static void main(String[] args) {
        // 使用匿名内部类
        new Swim() {
            @Override
            public void swimming() {
                System.out.println("自由泳...");
            }
        }.swimming();

        // 接口 变量 = new 实现类();
        // 多态,走子类的重写方法
        Swim s2 = new Swim() {
            @Override
            public void swimming() {
                System.out.println("蛙泳...");
            }
        };
        s2.swimming();
    }
}
6.匿名内部类的使用场景
  • 场景一

方法的形参类型是接口或者抽象类时,可以将匿名内部类作为实参传入。代码如下:

interface Swim {
    void swimming();
}
public class MyTest {
    public static void main(String[] args) {
        // 普通方式传入对象
        // 创建实现类对象
        Student s = new Student();

        goSwimming(s);
        // 匿名内部类使用场景:作为方法参数传递
        Swim s3 = new Swim() {
            @Override
            public void swimming() {
                System.out.println("蝶泳...");
            }
        };
        // 传入匿名内部类
        goSwimming(s3);

        // 完美方案: 一步到位
        goSwimming(new Swim() {
            @Override
            public void swimming() {
                System.out.println("大学生, 蛙泳...");
            }
        });

        goSwimming(new Swim() {
            @Override
            public void swimming() {
                System.out.println("小学生, 自由泳...");
            }
        });
    }

    // 定义一个方法,模拟请一些人去游泳
    public static void goSwimming(Swim s) {
        s.swimming();
    }
}
输出:
蝶泳...
大学生, 蛙泳...
小学生, 自由泳...
  • 场景二

一个类的构造构造方法受保护时,可以使用匿名内部类获取该类的属性和方法

package VO;
import lombok.Data;

public class Response {
    private String code="000";
    private String msg="成功";
    private UniqueObject uniqueObject;
  //构造方法受保护
    protected Response(){}
    public String getMsg(){
        return this.msg;
    }
    public String getCode(){
        return this.code;
    }
    /**
     * UniqueObject对象为Response类独拥有
     */
    public UniqueObject getUniqueObj(){
        return new UniqueObject();
    }
  
    class UniqueObject{}
}

2.1 实例1

import VO.Response;

public class JUC02 {
    public static void main(String[] args) {
        JUC02 juc02 = new JUC02();
        juc02.printResCode(new Response(){});
    }
    public void printResCode(Response res){
        System.out.println(res.getCode()); //输出000
    }
}

printResCode方法需要传入Response类型的对象,但是Response的构造方法为protected,所以不能直接创建Response对象,此时可以使用匿名内部类。juc02.printResCode(new Response(){});相当于

import VO.Response;

public class JUC02 {
    public static void main(String[] args) {
        JUC02 juc02 = new JUC02();
      //创建Response的子类对象
        Response myResponse = new MyResponse();
      //将子类对象传入
        juc02.printResCode(myResponse);
    }
    public void printResCode(Response res){
        System.out.println(res.getCode());
    }
}
//创建类来继承Response,MyResponse类拥有Response的所有属性和方法
class MyResponse extends Response{}

2.2 实例2

import VO.Response;

public class JUC02 {
    public static void main(String[] args) {
        JUC02 juc02 = new JUC02();
        juc02.consumeResUniqueObj(new Response(){}.getUniqueObj());
    }
    public void consumeResUniqueObj(Response.UniqueObject uniqueObject){
        System.out.println("正在消费Response独有的对象。。。"); //输出:正在消费Response独有的对象。。。
    }
}

consumeResUniqueObj方法需要传入Response.UniqueObject类型对象,而Response.UniqueObject类型对象只能通过Response对象的getUniqueObj方法获取,此时可以使用匿名内部类,然后调用匿名内部类的getUniqueObj方法。

new Response(){}.getUniqueObj()解析:创建一个匿名内部类,然后该类继承Response类,然后创建该匿名内部类对象,最后调用匿名内部类的getUniqueObj方法,因为匿名内部类没有重写getUniqueObj方法,所以此时调用的是父类Response的getUniqueObj方法。

2.3 实例3

package entity;

import lombok.Data;

@Data
public class Person<T> {
    private String name;
    private Integer age;
    private T obj;
}
package entity;

import lombok.AllArgsConstructor;
import lombok.Data;

@Data
@AllArgsConstructor
public class Car {
    private String brand;
    private Double price;
}

要将json格式的字符串{"name":"ming","age":1,"obj":{"brand":"benz","price":12.55}}转为对象Person<Car>,但是由于Person<Car>中存在泛型,所以不能使用类似JSONUtil.toBean(json, Person<Car>.class);的方法进行转换,此时可以使用Gson的fromJson方法。

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import entity.Car;
import entity.Person;

public class JUC02 {
    public static void main(String[] args) {
        Gson gson = new Gson();
        String str="{\"name\":\"ming\",\"age\":1,\"obj\":{\"brand\":\"benz\",\"price\":12.55}}";
         o = gson.fromJson(str, new TypeToken<Person<Car>>(){}.getType());
        System.out.println(o);//输出:Person(name=ming, age=1, obj=Car(brand=benz, price=12.55))
    }

TypeToken类的构造方法访问修饰符为protected,不能直接创建TypeToken类对象,``new TypeToken<Person>(){}.getType()表示创建一个匿名内部类继承TypeToken<Person>类,然后创建该匿名内部类的对象,然后调用getType()`方法。相当于如下代码:

import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import entity.Car;
import entity.Person;

public class JUC02 {
    public static void main(String[] args) {
        Gson gson = new Gson();
        String str="{\"name\":\"ming\",\"age\":1,\"obj\":{\"brand\":\"benz\",\"price\":12.55}}";
      //创建TypeToken的子类对象
        TypeToken<Person<Car>> myTypeToken = new MyTypeToken();
        Person<Car> o = gson.fromJson(str, myTypeToken.getType());//调用TypeToken子类对象getType()方法,因为子类没有重写getType()方法,所以实际上执行的是父类TypeToken<Person<Car>>的方法
        System.out.println(o);
    }
}
//创建子类继承TypeToken类,并指定泛型
class MyTypeToken extends TypeToken<Person<Car>>{}

Gson的做法非常巧妙,将需要获取类型的泛型类作为TypeToken的泛型参数构造一个匿名的子类,然后通过getType()方法就可以获取到我们想要的泛型类的泛型参数类型。可以理解为是将泛型类型存起来,解决了泛型擦除的问题。

  • 18
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值