Java内部类你真的会吗?

https://mp.weixin.qq.com/s/IDKUmQB5Na-dSFwDag0pmA

Java内部类你真的会吗?

Java架构研究室 5天前

一、四种内部类

1.1、成员内部类

成员内部类是最普通的内部类,它的定义为位于另一个类的内部,形如下面的形式:

 1 public class OuterAndInnerClass {
 2     public static void main(String[] args) {
 3         Outer outer = new Outer();
 4         //创建内部类的两种方式 (1)在外部
 5         //Outer.Inner inner = outer.new Inner();
 6         //创建内部类的两种方式 (1)在内部类所依附的外部类中创建
 7         Outer.Inner inner =outer.getInnerClass();
 8         outer.out();
 9         inner.in();
10         inner.testInner();
11     }
12 }
13 class Outer{//外部类
14     public Inner getInnerClass(){
15         return new Inner();
16     }
17     String outName = "外部类";
18     String sameName = "同名外部";
19     public void out(){
20         System.out.println("外部方法");
21     }
22     class Inner{//内部类
23         String inName = "内部类";
24         String sameName = "同名内部";
25         String name = "内部类变量";
26         public void in(){
27             System.out.println("内部方法");
28         }
29         public void testInner(){
30             String name = "局部变量";
31             System.out.println(name);//#内部类变量
32             System.out.println(this.name);//#局部变量
33             System.out.println("outName:" + outName);//#outName:外部类
34             System.out.println("inName:" + inName);//#inName:内部类
35             System.out.println("sameName:" + sameName);//#sameName:同名内部
36             System.out.println("sameName:" + this.sameName);//#sameName:同名内部,this指向Inner
37             System.out.println("sameName:" + Outer.this.sameName);//#sameName:同名外部
38         }
39     }
40 }

 

1.1.1,创建成员内部类的方法有两种

  虽然成员内部类可以无条件地访问外部类的成员,而外部类想访问成员内部类的成员却不是这么随心所欲了。在外部类中如果要访问成员内部类的成员,必须先创建一个成员内部类的对象,再通过指向这个对象的引用来访问:

Outer outer = new Outer();
第一种方式:Outer.Inner inner= outer.new Inner();
第二种方式:Outer.Inner inner= outer.getInnerClass();

 


1.1.2,成员内部类的访问控制修饰符

内部类就如同外部类的成员变量一样。四种访问控制符都是可以的,public,default,protected,private。
内部类可以拥有private访问权限、protected访问权限、public访问权限及包访问权限。比如上面的例子,如果成员内部类Inner用private修饰,则只能在外部类的内部访问,如果用public修饰,则任何地方都能访问;如果用protected修饰,则只能在同一个包下或者继承外部类的情况下访问;如果是默认访问权限,则只能在同一个包下访问。这一点和外部类有一点不一样,外部类只能被public和包访问两种权限修饰。我个人是这么理解的,由于成员内部类看起来像是外部类的一个成员,所以可以像类的成员一样拥有多种权限修饰。

1.1.3,成员内部类调用外部类的成员变量或者方法

调用内部类的成员变量:

System.out.println("inName:" + inName);//#inName:内部类

调用外部类的成员变量:(同调用内部类的成员变量)

System.out.println("outName:" + outName);//#outName:外部类
TIPS:
    特殊情况:当外部类和内部类的成员变量同名的情况
    当成员内部类拥有和外部类同名的成员变量或者方法时,会发生隐藏现象,即默认情况下访问的是成员内部类的成员。如果要访问外部类的同名成员,需要以下面的形式进行访问:
    外部类.this.成员变量
    外部类.this.成员方法

1.2、局部内部类

局部内部类是定义在一个方法或者一个作用域里面的类,它和成员内部类的区别在于局部内部类的访问仅限于方法内或者该作用域内。

 

 1 //局部内部类
 2 public class LocalInnerClass {
 3     public static void main(String[] args) {
 4         People people = new People();
 5         people.getWoman("形参变量");
 6     }
 7 }
 8 class People {
 9     String peopleName = "people";
10     String sameName = "外部同名变量";
11     public People getWoman(final String methodName){
12         final String localName = "局部变量";
13         class Woman extends People{
14             String womanName = "woman";
15             String sameName = "局部内部类同名变量";
16             public Woman(){
17                 //methodName = "";//编译错误:Cannot assign a value to final variable 'methodName'
18                 //localName = "";//编译错误:Variable 'localName' is accessed from within inner class, needs to be final or effectively final
19                 System.out.println(methodName);//#形参变量
20                 System.out.println(localName);//#局部变量
21                 System.out.println(peopleName);//#people
22                 System.out.println(womanName);//#woman
23                 System.out.println(sameName);//#局部内部类同名变量
24                 System.out.println(this.sameName);//#局部内部类同名变量
25                 System.out.println(People.this.sameName);//#外部同名变量
26             }
27         }
28         return new Woman();
29     }
30 }

 

在局部内部类中调用外部类的变量或者方法的方式和规则是一样的。

TIPS
    值得注意的是,局部内部类就像是方法里面的一个局部变量一样,是不能有public、protected、private以及static修饰符的。

 

1.3、匿名内部类

  匿名内部类由于没有名字,所以它的创建方式有点儿奇怪。创建格式如下:

new 父类构造器(参数列表)|实现接口()
    {
     //匿名内部类的类体部分
    }

 

  在这里我们看到使用匿名内部类我们必须要继承一个父类或者实现一个接口,当然也仅能只继承一个父类或者实现一个接口。同时它也是没有class关键字,这是因为匿名内部类是直接使用new来生成一个对象的引用。当然这个引用是隐式的。

 

 1 //匿名内部类
 2 public class AnonInnerClass {
 3     public static void useRunnable(MyRunnable runnable){
 4         runnable.run();
 5     }
 6     public static void main(String[] args) {
 7         AnonInnerClass.useRunnable(new MyRunnable() {
 8             @Override
 9             public void run() {
10                 System.out.println("重写run方法");
11             }
12         });
13         AnonInnerClass.useRunnable(new MyRunnable("name") {
14             @Override
15             public void run() {
16                 System.out.println("重写run方法");
17             }
18         });
19     }
20 }
21 abstract class MyRunnable {
22     public MyRunnable(){
23         System.out.println("调用匿名内部类的无参构造器");
24     }
25     public MyRunnable(String name){
26         System.out.println("调用匿名内部类的有参构造器,参数为:" + name);
27     }
28     //抽象方法
29     public abstract void run();
30 }

 

 

  这里我们能够看到,useRunnable 方法要接受一个MyRunnable的实例参数,但是,MyRunnable是一个抽象的类,不能被实例化,所以只能创建一个新的类继承这个MyRunnable类,然后指向MyRunnable,从而拿到MyRunnable的实例参数。
  在这里JVM会创建一个继承自MyRunnable类的匿名类的对象,该对象转型为对MyRunnable类型的引用。
  对于匿名内部类的使用它是存在一个缺陷的,就是它仅能被使用一次,创建匿名内部类时它会立即创建一个该类的实例,该类的定义会立即消失,所以匿名内部类是不能够被重复使用。对于上面的实例,如果我们需要对test()方法里面内部类进行多次使用,建议重新定义类,而不是使用匿名内部类。

 

 

TIPS:
1、使用匿名内部类时,我们必须是继承一个类或者实现一个接口,但是两者不可兼得,同时也只能继承一个类或者实现一个接口。
2、匿名内部类中是不能定义构造函数的。(类都是匿名的,没法定义构造方法)
3、匿名内部类中不能存在任何的静态成员变量和静态方法。(类是匿名的,当然没有类方法或类变量)
4、匿名内部类为局部内部类,所以局部内部类的所有限制同样对匿名内部类生效。
5、匿名内部类不能是抽象的,它必须要实现继承的类或者实现的接口的所有抽象方法。
6、我们给匿名内部类传递参数的时候,若该形参在内部类中需要被使用,那么该形参必须要为final。也就是说:当所在的方法的形参需要被内部类里面使用时,该形参必须为final。
7、匿名内部类的初始化(使用构造代码块)

 

 

我们一般都是利用构造器来完成某个实例的初始化工作的,但是匿名内部类是没有构造器的!那怎么来初始化匿名内部类呢?使用构造代码块!利用构造代码块能够达到为匿名内部类创建一个构造器的效果。

 

 1 public class InitAnonInnerClass {
 2     public static void main(String[] args) {
 3         OuterClass outer = new OuterClass();
 4         InnerClass inner1 = outer.getInnerClass(15, "变了");
 5         System.out.println(inner1.getStr());
 6         InnerClass inner2 = outer.getInnerClass(20, "变了");
 7         System.out.println(inner2.getStr());
 8     }
 9 }
10 
11 class OuterClass {
12     public InnerClass getInnerClass(final int num, final String str){
13         return new InnerClass() {
14             int num_ ;
15             String str_ ;
16             //使用构造代码块完成初始化
17             {
18                 if(0 < num && num < 18){
19                     //str = "";//编译错误Variable 'str' is accessed from within inner class, needs to be final or effectively final
20                     str_ = str;
21                 }else {
22                     str_ = "没变啊";
23                 }
24             }
25             public String getStr(){
26                 return str_;
27             }
28         };
29     }
30 }
31 
32 abstract class InnerClass {
33     public abstract String getStr();
34 }
out:

  变了
  没变啊

 

 

 

1.4、静态内部类

静态内部类也是定义在另一个类里面的类,只不过在类的前面多了一个关键字static。静态内部类是不需要依赖于外部类的,这点和类的静态成员属性有点类似,并且它不能使用外部类的非static成员变量或者方法,这点很好理解,因为在没有外部类的对象的情况下,可以创建静态内部类的对象,如果允许访问外部类的非static成员就会产生矛盾,因为外部类的非static成员必须依附于具体的对象。

 

 1 //静态内部类
 2 public class StaticInnerClass {
 3     public static void main(String[] args) {
 4         //初始化静态内部类,注意和其它内部类的初始化方式的区别
 5         OuterClass1.InnerClass inner = new OuterClass1.InnerClass();
 6         inner.test();
 7     }
 8 }
 9 class OuterClass1 {
10     String outName = "我是外部类";
11     static String outType = "外部类";
12     static class InnerClass {
13         String innerName = "我是内部类";
14         static String innerType = "静态内部类";
15         public InnerClass (){
16             //System.out.println(outName);//编译错误:Non-static field 'outName' cannot be referenced from a static context
17             System.out.println(outType);
18         }
19         public void test(){
20             System.out.println("调用内部类方法");
21         }
22     }
23 }

 

 

如上所示创建静态内部类对象的一般形式为: 

外部类类名.内部类类名 xxx = new 外部类类名.内部类类名()

 

二、深入理解内部类

2.1.为什么成员内部类可以无条件访问外部类的成员?

  在此之前,我们已经讨论过了成员内部类可以无条件访问外部类的成员,那具体究竟是如何实现的呢?下面通过反编译字节码文件看看究竟。事实上,编译器在进行编译的时候,会将成员内部类单独编译成一个字节码文件,下面是OuterAndInnerClass.java的代码:

 

 1 public class OuterAndInnerClass {
 2     public static void main(String[] args) {
 3         Outer outer = new Outer();
 4         Outer.Inner inner = outer.getInnerClass();
 5         outer.out();
 6     }
 7 }
 8 
 9 class Outer{
10     public Inner getInnerClass(){
11         return new Inner();
12     }
13     public void out(){
14         System.out.println("外部方法");
15     }
16     class Inner{
17         public void in(){
18             System.out.println("内部方法");
19         }
20     }
21 }

 

 

编译之后,出现了两个字节码文件:

  编译器会默认为成员内部类添加了一个指向外部类对象的引用,那么这个引用是如何赋初值的呢?
虽然我们在定义的内部类的构造器是无参构造器,编译器还是会默认添加一个参数,该参数的类型为指向外部类对象的一个引用,所以成员内部类中的Outter this&0 指针便指向了外部类对象,因此可以在成员内部类中随意访问外部类的成员。从这里也间接说明了成员内部类是依赖于外部类的,如果没有创建外部类的对象,则无法对Outter this&0引用进行初始化赋值,也就无法创建成员内部类的对象了。

 2.2、为什么局部内部类和匿名内部类只能访问局部final变量?

 

 1 public class Test {
 2     public static void main(String[] args)  {
 3         Test test = new Test();
 4         test.test(1);
 5     }
 6 
 7     public void test(final int b) {
 8         final int a = 10;
 9         new Thread(){
10             public void run() {
11 //                a = 2;//编译错误:Cannot assign a value to final variable 'b'
12 //                b = 3;//编译错误:Cannot assign a value to final variable 'b'
13                 System.out.println(a);
14                 System.out.println(b);
15             };
16         }.start();
17     }
18 }

 

  当test方法执行完毕之后,变量a的生命周期就结束了,而此时Thread对象的生命周期很可能还没有结束,那么在Thread的run方法中继续访问变量a就变成不可能了,但是又要实现这样的效果,怎么办呢?Java采用了 复制 的手段来解决这个问题。这个过程是在编译期间由编译器默认进行,如果这个变量的值在编译期间可以确定,则编译器默认会在匿名内部类(局部内部类)的常量池中添加一个内容相等的字面量或直接将相应的字节码嵌入到执行字节码中。这样一来,匿名内部类使用的变量是另一个局部变量,只不过值和方法中局部变量的值相等,因此和方法中的局部变量完全独立开。

  也就说如果局部变量的值在编译期间就可以确定,则直接在匿名内部里面创建一个拷贝。如果局部变量的值无法在编译期间确定,则通过构造器传参的方式来对拷贝进行初始化赋值。
  从上面可以看出,在run方法中访问的变量a根本就不是test方法中的局部变量a。这样一来就解决了前面所说的 生命周期不一致的问题。但是新的问题又来了,既然在run方法中访问的变量a和test方法中的变量a不是同一个变量,当在run方法中改变变量a的值的话,会出现什么情况?
  对,会造成数据不一致性,这样就达不到原本的意图和要求。为了解决这个问题,java编译器就限定必须将变量a限制为final变量,不允许对变量a进行更改(对于引用类型的变量,是不允许指向新的对象),这样数据不一致性的问题就得以解决了。
  到这里,想必大家应该清楚为何 方法中的局部变量和形参都必须用final进行限定了。

 

文章到此就结束了,如果你觉得文章还不错,欢迎大家关注下方公众号查看更多干货好文,感谢你的阅读。

【基于Python的大麦网自动抢票工具的设计与实现】 随着互联网技术的发展,网络购票已经成为人们生活中不可或缺的一部分。尤其是在文化娱乐领域,如音乐、演唱、戏剧等活动中,热门演出的门票往往在开售后瞬间就被抢购一空。为了解决这个问题,本论文探讨了一种基于Python的自动抢票工具的设计与实现,旨在提高购票的成功率,减轻用户手动抢票的压力。 Python作为一种高级编程语言,因其简洁明了的语法和丰富的第三方库,成为了开发自动化工具的理想选择。Python的特性使得开发过程高效且易于维护。本论文深入介绍了Python语言的基础知识,包括数据类型、控制结构、函数以及模块化编程思想,这些都是构建抢票工具的基础。 自动化工具在现代社中广泛应用,尤其在网络爬虫、自动化测试等领域。在抢票工具的设计中,主要利用了自动化工具的模拟用户行为、数据解析和定任务等功能。本论文详细阐述了如何使用Python中的Selenium库来模拟浏览器操作,通过识别网页元素、触发事件,实现对大麦网购票流程的自动化控制。同,还讨论了BeautifulSoup和requests库在抓取和解析网页数据中的应用。 大麦网作为国内知名的票务平台,其网站结构和购票流程对于抢票工具的实现至关重要。论文中介绍了大麦网的基本情况,包括其业务模式、用户界面特点以及购票流程,为工具的设计提供了实际背景。 在系统需求分析部分,功能需求主要集中在自动登录、监控余票、自动下单和异常处理等方面。抢票工具需要能够自动填充用户信息,实监控目标演出的票务状态,并在有票立即下单。此外,为了应对可能出现的网络延迟或服务器错误,工具还需要具备一定的错误恢复能力。性能需求则关注工具的响应速度和稳定性,要求在大量用户同使用仍能保持高效运行。 在系统设计阶段,论文详细描述了整体架构,包括前端用户界面、后端逻辑处理以及与大麦网交互的部分。在实现过程中,采用了多线程技术以提高并发性,确保在抢票关键环节的快速响应。此外,还引入了异常处理机制,以应对网络故障或程序错误。 测试与优化是确保抢票工具质量的关键步骤。论文中提到了不同场景下的测试策略,如压力测试、功能测试和性能测试,以验证工具的有效性和稳定性。同,通过对抢票算法的不断优化,提高工具的成功率。 论文讨论了该工具可能带来的社影响,包括对消费者体验的改善、对黄牛现象的抑制以及可能引发的公平性问题。此外,还提出了未来的研究方向,如增加多平台支持、优化抢票策略以及考虑云服务的集成,以进一步提升抢票工具的实用性。 本论文全面介绍了基于Python的大麦网自动抢票工具的设计与实现,从理论到实践,从需求分析到系统优化,为读者提供了一个完整的开发案例,对于学习Python编程、自动化工具设计以及理解网络购票市场的运作具有重要的参考价值。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值