Java基础(二)—— 内部类

最近这段时间稍微会有点忙,也就到周末晚上抽出点时间来补上文章。今天这个主题也是才刚想好。主要是前几天刚好看到对“类”这部分的介绍,平时工作的时候没有特别注意这么细节,现在才发现,类的种类要真的细分的话还是挺多的。

 

首先,类是什么?这个相信接触过面向对象的小伙伴都或多或少对这句话有点印象:类是描述了一组有相同特性(属性)和相同行为(方法)的一组对象的集合。这句话有点抽象,但实际上类就是一种抽象的概念,它是可以你根据任意规律来定义的。比如:张三,李四。这两个人,只要他们有相同的规律(即,相同的特性和行为),都可以定义成类。

  1. 他们都是人类,都会有特性:姓名,性别。也都有行为:说话,吃饭。根据这组规律就能定义类:Person。
  2. 他们都是学生,都会有特性:班级,座位。也都有行为:上课,写作业。根据这组规律就能定义类:Student。
  3. 又他们都是员工,都会有特性:岗位,薪资。也都有行为:工作,摸鱼。根据这组规律就能定义类:Employee。

......

 

如此总总,我们可以根据自己的需要,为他们找出相同的规律,然后定义自己需要的类。

 

类就是这么一个非常抽象的概念,实际的个体(张三,李四)呢,就是根据类创建的一个个的对象实例了。那么Java中又存在哪些类可以方便我们使用?

 

大体上可以分类以下几种:

  1. 外部类
  2. 内部类
    1. 成员内部类
    2. 静态内部类
    3. 局部内部类
    4. 内部匿名类

当然,根据权限修饰符和final关键词组合的话也还能衍生出其他更细的类,具体这块就在之后哪期想聊这方面了再细讲吧。本期只关注类本身。

外部类

外部类就是非常普通,平常开发中第一要用到的类。像main方法所在的启动类就是一个很普通的外部类。可以使用权限修饰符(public,空)和final修饰符修饰。

public class Person {

    /**
     * 姓名
     */
    private String name;

    /**
     * 性别
     */
    private Integer sex;

    public String say(){
        // say something
        return null;
    }

    public void eat(){
        // eat something
    }
    
    ````
	getter
    setter
    ````
}

注意项:

  1. 外部类不能使用static关键词修饰
  2. 权限修饰符有且只有public和空两种。如果用public修饰了,则文件名必须和类名一致,反过来说如果不是public,则类名可以和文件名不相同(注:实际开发中别这么玩)
  3. 同一个文件中只会存在一个public修饰的类(即,同一个文件中可以定义多个类,但是只会有一个是public)。结构如下:
public class Person {
    // 特性和行为忽略
}

class Student {
    // 特性和行为忽略
}

内部类

内部类又是什么呢?见名知义,内部类就是定义在内部的类,再具体点就是定义在类的内部或者块的内部。而定义在类的内部又可以再细化为成员内部类静态内部类,定义在块的内部就是局部内部类,而匿名内部类呢比较特殊,此处不细讲,我们在后续的环节再揭晓答案。

 

成员内部类

这种类是定义在类内部的一种类,为什么叫成员类呢?这是因为这种类与它所属外部类的成员一样拥有外部类的实例对象引用(有些地方叫隐式引用)。具体结构如下:

public class Person {
    // 特性和行为忽略
    
    [private | protected | public] class Student {
    	// 特性和行为忽略
	}
}

如上所示,Student为Person的成员内部类,既然带了成员二字,那自然应该想到类的成员属性和成员方法,与它们一样,成员内部类也有四种权限修饰符private/protected/public/空,相应的访问权限也一样,具体访问修饰符放下期再详细讲一讲。

 

为什么要定义内部类?

  1. 内部类可以对同一个包中的其他类隐藏
  2. 内部类可以访问他所属外部类的属性和方法,包括私有。

 

有没有小伙伴好奇下,为什么内部类可以访问外部类的私有成员属性,是不是JVM做了什么特别的操作,以至于类之间可以跨越权限修饰访问到私有域。其实关于这个问题,并不是JVM处理的,对于JVM来说外部类和内部类没有什么本质的区别,内部类只是一种名字比较特别的类而已,实际的魔术是编译器变的

 

编译器在编译时会在内部类中生成一个外部类类型的成员属性,然后在内部类构造时传入外部类的实例,并且有访问外部类的私有属性时同样会在外部类生成一个同包可访问的静态方法用于返回外部类的私有属性的值,入参为外部类的实例,再将访问外部类私有属性的地方替换为调用该方法。

 

详细结构如下:

假设源代码为:

public class Person {
    private int i = 2;
    
    class Student {
    	public void action() {
            System.out.println(i);
        }
	}
}

编译之后其实成了这样:可以使用javap -private来看编译后的文件

Compiled from "Person.java"
public class org.aischen.Person {
  private int i;
  public org.aischen.Person();
  static int access$000(org.aischen.Person);
}
Compiled from "Person.java"
class org.aischen.Person$Student {
  final org.aischen.Person this$0;
  org.aischen.Person$Student(org.aischen.Person);
  public void action();
}

 

可以看到Person这个外部类多了一个access$000的静态方法(也可能为access$0方法,取决于具体的编译器),这个方法就是在Student这个内部类中调用私有属性i时的替代物。而且Student类新增了一个this$0私有属性用来接收外部类的实例,由构造方法接收。另外有个特别的点是,当内部类为的权限修饰符为private时,编译后将会出现两个构造方法:

Compiled from "Person.java"
class org.aischen.Person$Student {
  final org.aischen.Person this$0;
  private org.aischen.Person$Student(org.aischen.Person);
  public void action();
  org.aischen.Person$Student(org.aischen.Person, org.aischen.Person$1);
}

一个构造方法与类的权限相同都是private,另外还有一个无权限修饰符的构造方法,这个构造方法将调用第一个private的构造方法,之所以多了一个参数只是为了和其他构造方法区分开而已。

 

编译器这样做不是存在安全风险吗?这种担心当然是有道理的,任何人都可以调用access$000方法访问私有属性,甚至通过org.aischen.Person$Student(org.aischen.Person, org.aischen.Person$1);构造内部私有类的实例。当然这种手段还是有点成本的,需要熟悉类文件的结构,并且通过十六进制编辑器创建类文件,还得和被攻击的类放在同一个包下。所以真的想要攻击的话需要技巧和决心。

 

这种内部类有什么限制吗?还真的有:

  1. 内部类中声明的所以静态变量都必须是final,并初始化一个编译时常量
  2. 内部类中不能存在静态方法。至于为何,Java语言规范对此没有做任何解释。

 

特殊语法规则:

  1. 访问外部类的属性或方法。可以使用外部类.this得到外部类的实例引用,然后就是正常的访问实例的属性或者方法了。例如访问Person中的i可以写作:Person.this.i
  2. 初始化内部类实例,则需要先初始化外部类实例,然后通过外部类实例.new调用内部类的构造方法。例如:Person person = new Person(); Student student = person.new Student();之后使用方法与普通的实例就没有什么区别了。

 

静态内部类

静态内部类与成员内部类的最本质的区别就是加了static修饰符修饰。和类的其他静态方法或者静态成员概念一样,类的静态内部类也是属于类而不属于它的实例,因此调用方式也是通过类名.内部类名。例如现在源码长这个样子:

public class Person {
    // 特性和行为忽略
    
    [private | protected | public] static class Student {
    	// 特性和行为忽略
	}
}

那么同样的想要构造Student时如下:Student student = new Person.Student();其他使用上和普通类没有什么区别,一样可以有静态变量和静态方法。与成员内部类不同的是,静态内部类不会有外部类的引用,当然如果是访问私有属性和私有方法的,与成员内部类处理一致,也是生成一个包可访问的方法access$000,以供访问。

 

同样的,静态内部类也有一些需要注意的点:

  1. 只要内部类不想要访问外部类的实例对象时,就应该使用静态内部类。
  2. 静态内部类可以有静态变量和静态方法。
  3. 接口中声明的内部类自动是static和public。

 

局部内部类

什么是局部内部类呢?定义在块中(方法块,代码块等)的类就是局部内部类。局部内部类有个特点,它不能加访问修饰符(权限修饰符或者static等),只有定义这个类的块可以访问该类。局部内部类拥有成员内部类同样的缺点和优点,且局部内部类还可以访问局部变量,当然这有个限制,该局部变量必须是事实最终变量。事实最终变量的意思就是只会有一次赋值,赋值之后不再改变的变量。

 

同样的,局部内部类能够访问到局部变量,也是编译器做了手脚,在构造局部内部类的实例时将局部变量传入。以此来获得局部变量的访问权限。

 

匿名内部类

匿名内部类也就是匿名类,通常使用在构造接口或者抽象类的实例时,我们应该知道接口和抽象类是不能构造实例的,因此当我们构造接口或者抽象类的实例时需要将抽象方法的方法体补上,这时编译器会自动帮我们创建一个匿名类(意为没有名字的类),具体的代码可能如下:

new ActionListener() {
    @Override
    public void actionPerformed(ActionEvent e) {

    }
};

这种构造方法后面跟了大括号的形式就是定义一个匿名类,编译器会编译出名字可能为:Test$1.class的类文件。它是实现了ActionListener接口的一个实现类。当然也可以根据普通类创建匿名类,例如:

new Person(){};

这种就是定义了一个扩展了Person的匿名类。

 

匿名类有一个特别的技巧叫做“双括号初始化”,其实我理解就是在内部类定义了个初始块,在实例化时会调用而已。例如构造一个ArrayList:

List<Integer> arrayList = new ArrayList<>();
arrayList.add(1);
arrayList.add(2);
arrayList.add(3);
parse(arrayList);

如果使用“双括号初始化”可以写作:

parse(new ArrayList<Integer>(){{
    add(1);
    add(2);
    add(3);
}});

 

另外如果是在静态方法中想要获取到当前类名的话,直接用getClass()方法是会编译报错的,此时就可以使用匿名内部类了,可以直接用代码:new Object(){}.getClass().getEnclosingClass(); 它可以获取到这个匿名类的外部类,也就是包含静态方法的类。

 

不过,总体来说,在1.8新增了lambda表达式之后,内部类使用的场景就越来越少了。作者实际开发中还是lambda使用较多,内部类只有在类内部需要接收多个返回参数时会使用。

 

最后

这次的分享到这也就告一段落了,最近一段时间忙着做新业务,晚上下班到家也到很晚了,所以欠了这么久的文章到现在才补上,而且这周的文章都还没补齐,学习进度也停在原地没有动,感慨一下现在时间真的是不够用了。不过,无论如何,承诺一周一篇就得一周一篇,欠的一篇稍后再补上,也许忙过这阵子会有个时间空窗期再好好制定下计划,追一下学习进度。也希望看到的读者朋友们能积极向上和作者一起成长,督促我也督促自己,让我们一起变得更加强大。

posted @   aischen  阅读(75)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
点击右上角即可分享
微信分享提示