精华内容
下载资源
问答
  • java成员内部类
    万次阅读 多人点赞
    2021-11-20 17:06:25

    🍅 作者简介:哪吒,CSDN2021博客之星亚军🏆、新星计划导师✌、博客专家💪

    🍅 哪吒多年工作总结:Java学习路线总结,搬砖工逆袭Java架构师

    🍅 关注公众号【哪吒编程】,回复1024,获取Java学习路线思维导图、大厂面试真题、加入万粉计划交流群、一起学习进步

    更多相关内容
  • JAVA内部类之成员内部类

    千次阅读 2021-03-09 06:39:17
    如何定义成员内部类那要如何定义成员内部类呢。既然叫成员内部类,说明和成员变量是有类似的地方。从代码的层次结构上来看他是和成员变量处于相同层级的。我们来举个例子。上图代码中,在类OutClass中定义了一个成员...

    JAVA有一个特殊的类形式——内部类(这个词有点为难普通话不好的南方朋友)。今天我们就来聊聊内部类的一种:成员内部类。

    1b82d64bf337e71b19a0c8ee52fd8a21.png

    如何定义成员内部类

    那要如何定义成员内部类呢。

    既然叫成员内部类,说明和成员变量是有类似的地方。从代码的层次结构上来看他是和成员变量处于相同层级的。

    我们来举个例子。

    e280c93b992f2035938c974f29c2c5d0.png

    上图代码中,在类OutClass中定义了一个成员变量name。在OutClass中又定义了一个类InClass,这个InClass是和OutClass的成员变量是同一个层级的,并且是非静态的。像InClass这样的类,我们就称之为成员内部类。

    如何使用内部类

    那这种定义在一个类中的内部类要怎么使用呢。

    我这里从两个角度来说这个使用的方式。

    第一种是,在外部类(如示例代码OutClass中)使用。下面举个例子。

    82d691de839e941136365ba6e5859559.png

    上图代码中,在OutClass类中,定义了一个InClass的成员变量,这样的内部类使用方式从编码的形式来说和使用其他类是没有什么差别的。

    第二种使用方式是在其他类中使用。也来举个例子。

    fd6912c87f45efc0995be4e2da10f3cd.png

    如图中代码所示,要在其他的类中使用内部类,是需要通过外部类的对象才行,因为这个内部类是外部类对象的成员。

    不过在其他类中想要这么使用内部类的前提是,内部类定义的访问权限是可以让外部访问才行,我们试试吧内部类的访问修饰符改一下。

    98e5f9b770599be44ff022b81b01815d.png

    如上面代码所示,将内部类的访问修饰符改为private之后,在其他类中使用时会因为访问不到而报错。也就是说成员内部类是可以和成员变量一样,隐藏在本类内部而只是暴露一定的功能出去的。

    内部类访问外部类的成员变量、方法

    我们知道一个类中访问本类的成员变量、方法可以通过this关键词来访问。那如果内部类要访问外部类要怎么做呢。像上面例子中外部类的成员变量名和内部类的成员变量名重名会不会有什么问题呢(示例中外部类和内部类都有名为name的成员变量)。废话不多说,上代码。

    14df9cbdc935399350f91f839fbf1f83.png

    如上图代码中,内部类要访问外部类的成员变量或者方法,可以通过“外部类名.this.变量名或方法名”(如代码中的OutClass.this.test()、OutClass.this.name)。并且即便外部类的成员变量或方法是私有的,也是可以访问的。另外内部类和外部类的成员变量名重名是没有问题的。执行验证一下。

    658275232949861db3afcb26df7b7a7d.png

    成员内部类不能有静态变量、方法

    因为成员内部类是作为外部类的对象的成员来使用的,所以成员内部类中是不能有静态变量、方法的。

    d0959e2da67fb1c0723cbe209a75007f.png

    如上图中,我把上面的示例代码中的内部类的变量改成了静态的,会报错。添加了一个静态的方法,也是会报错的。

    静态的内容就到了这里了,砖我已经抛好了,希望引来各位的玉。

    展开全文
  • java 成员内部类的使用规范 java 成员内部类的使用规范
  • Java -- 成员内部类不能含有static成员(汇总)

    千次阅读 多人点赞 2020-11-15 14:14:51
    文章目录1、为什么内部类中有static成员时内部类也必须声明为static2、JVM的类加载规则 :3、成员内部类中为什么不能有静态方法和属性4、静态内部类(public static class)和普通内部类(public class)的区别5、Java中...

    1、为什么内部类中有static成员时内部类也必须声明为static

            如果A和B类关系紧密,且A类的主要作用是帮助完成B类的实现,这时可将A类作为B类的内部类,两个类可以互相访问各自的私有成员,这样就方便B类的设计,使B类更加自给自足(self contained)。http://www.tanhuanyao.com http:// tanhuanyao.com http://www.bianshayao.com/ http://bianshayao.com/这是我理解的内部类产生的原因,有点类似于C++中的友元类 A类作为B类的内部类,便是B类的一份子,地位上和B类的属性和方法相当,此时A类便有static和非static之分了,这就是为什么只有内部类的类本身才有static的说法的原因。内部类声明为static和普通类的成员为static的含义一样,都是表示被声明为static的东西属于类范畴,不依赖于类的具体对象。如果内部类是非static的,那么这个内部类就依赖于外部类的具体对象,在该内部类中可以调用外部类的非static方法;如果内部类是static的,即该内部类属于外部类的类范畴,不依赖于外部类的具体对象,那么该内部类只能调用外部类的static方法,因为外部类的非static方法依赖于外部类的具体对象。这和普通类中static方法只能访问static的属性和方法的道理是一样的。那么为什么规定内部类中如果出现了static方法,该内部类也必须声明为static呢? 内部类中的static方法表明该方法不依赖于内部类的具体对象,属于内部类的类范畴,假设此时内部类为非static的,那么内部类对象的产生就依赖于外部类对象,有一个外部类对象,才能有一个与之对应的内部类对象,而内部类对象中的static方法不依赖于内部类对象,所以使用该static方法便没有必要创建具体的内部类对象,如果该static方法确有其存在的意义,那就说明该内部类完全没有必要是非static的,java规定这时该内部类必须是static的。

    2、JVM的类加载规则 :

    1. static类型的属性和方法,在类加载的时候就会存在于内存中。
    2. 要想使用某个类的static属性和方法,那么这个类必须要加载到JAVA虚拟机中。
    3. 非静态内部类并不随外部类一起加载,只有在实例化外部类之后才会加载。现在考虑这个情况:在外部类并没有实例化,内部类还没有加载,这时候如果调用内部类的静态成员或方法,内部类还没有加载,试图在内存中创建该内部类的静态成员,这明显是矛盾的。所以非静态内部类不能有静态成员变量或静态方法。假设 :在外部类并没有实例化,内部类还没有加载,这时候如果JVM加载静 态成员或方法,内部类还没有加载,因为非静态内部类的加载依赖于实化,而此时却试图在内存中创建该内部类的静态成员,这明显是矛盾的。所以非静态内部类不能有静态成员变量或静态方法。

    3、成员内部类中为什么不能有静态方法和属性

    非静态内部类不能有静态成员!

    成员内部类必须先实例化外部类对象然后再实例化成员内部类;

    非static的内部类,在外部类加载的时候,并不会加载它,所以它里面不能有静态变量或者静态方法。

    1. static类型的属性和方法,在类加载的时候就会存在于内存中。
    2. 要使用某个类的static属性或者方法,那么这个类必须要加载到jvm中。
      基于以上两点,可以看出,如果一个非static的内部类如果具有static的属性或者方法,那么就会出现一种情况:内部类未加载,但是却试图在内存中创建static的属性和方法,这当然是错误的。原因:类还不存在,但却希望操作它的属性和方法。

    java很多想这类不能共同存在的 一般都与他们的生命周期有关。。。
    比如 静态成员和静态方法是随着类的加载而存在的,也就是说内部类的静态属性是随着类的加载的,但是内部类的实例 是创建后才存在的,也就是说其静态属性优先存在于他的类实例的存在 这显然是矛盾的,所以要把内部类设为静态的 这样他们的生命周期就是相同了;

    如果内部类没有static的话,就需要实例化内部类才能调用,说明非static的内部类不是自动跟随主类加载的,而是被实例化的时候才会加载。
    而static的语义,就是主类能直接通过内部类名来访问内部类中的static方法,而非static的内部类又是不会自动加载的,所以这时候内部类也要static,否则会前后冲突。

    4、静态内部类(public static class)和普通内部类(public class)的区别

    java中普通的顶级类是不能使用static关键字修饰的。

    只有内部类可以使用static修饰,或者不使用staitc关键字修饰。

    // 顶层类A不能用static修饰
    public class A{
       
       // 普通内部类B, 可以不用static修饰
       public class B{    
       }
     
       // 普通内部类C, 也可以用static修饰
       public static class C{    
       }
     
    }
    

    1、静态内部类(static修饰的内部类)没有对外部类的引用,所以静态内部类只能访问外部类的静态属性或方法。并且在初始化的时候可以单独存在,例如:

    StaticClass staticClass = new StaticClass();

    或者:

    Users.StaticClass staticClass2 = new Users.StaticClass()

    二种方式初始化,建议使用第二种初始化方法,比较清晰。

    2、普通内部类有对外部类的引用,所以普通内部类不能独立存在,初始化的时候必须通过外部类的实例。

    并且普通内部类可以直接访问外部类的普通属性和函数(包括私有的属性和函数)同时也能访问外部类的静态属性和函数。

    普通内部类的实例化如下:

    Users.CommonClass commonClass = new Users().new CommonClass();

    或者:

    Users users = new Users();

    Users.CommonClass commonClass2 = users.new CommonClass();

    5、Java中普通内部类为何不能有static属性 却可以有常量

    public class Outer{
        int x;
        class Inner{
            static int a = 0;//这样写是不合法的.
            static final int b=0;//这样写是合法的
        }
    }
    

    java类加载顺序,首先加载类,执行static变量初始化,接下来执行对象的创建,如果我们要执行代码中的变量int a 初始化,那么必须先执行加载外部类,再加载内部类,最后初始化静态变量 a ,问题就出在加载内部类上面,我们可以把内部类看成外部类的非静态成员,它的初始化必须在外部类对象创建后以后进行,要加载内部类必须在实例化外部类之后完成 ,java虚拟机要求所有的静态变量必须在对象创建之前完成, 这样便产生了矛盾。

    将class Inner看作外部类的非静态成员,它属于对象Outer(即new 出来的Outer()),static int a 属于类Inner 所以先于对象Inner Outer创建出来,矛盾

    而java常量放在内存中常量池,它的机制与变量是不同的,编译时,加载常量是不需要加载类的,所以就没有上面那种矛盾。

    静态的东西需要在编译时分配内存,而非静态内部类是在实例化的时候才分配内存。所以现在的情况就是有了东西没地方放 static int a 没地方放,因为class Inner还没有实例化

    6、java内部类有什么好处?为什么需要内部类?

    提起Java内部类(Inner Class)可能很多人不太熟悉,实际上类似的概念在C++里也有,那就是嵌套类(Nested Class),关于这两者的区别与联系,在下文中会有对比。内部类从表面上看,就是在类中又定义了一个类(下文会看到,内部类可以在很多地方定义),而实际上并没有那么简单,乍看上去内部类似乎有些多余,它的用处对于初学者来说可能并不是那么显著,但是随着对它的深入了解,你会发现Java的设计者在内部类身上的确是用心良苦。学会使用内部类,是掌握Java高级编程的一部分,它可以让你更优雅地设计你的程序结构。下面从以下几个方面来介绍:

    public interface Contents {
    	int value();
    }
    public interface Destination {
    	String readLabel();
    }
    public class Goods {
    	private class Content implements Contents {
    		private int i = 11;
    		public int value() {
    			return i;
    		}
    	}
    	protected class GDestination implements Destination {
    		private String label;
    		private GDestination(String whereTo) {
    			label = whereTo;
    		}
    		public String readLabel() {
    			return label;
    		}
    	}
    	public Destination dest(String s) {
    		return new GDestination(s);
    	}
    	public Contents cont() {
    		return new Content();
    	}
    }
    class TestGoods {
    	public static void main(String[] args) {
    		Goods p = new Goods();
    		Contents c = p.cont();
    		Destination d = p.dest("Beijing");
    	}
    }
    

    在这个例子里类Content和GDestination被定义在了类Goods内部,并且分别有着protected和private修饰符来控制访问级别。Content代表着Goods的内容,而GDestination代表着Goods的目的地。它们分别实现了两个接口Content和Destination。在后面的main方法里,直接用 Contents c和Destination d进行操作,你甚至连这两个内部类的名字都没有看见!这样,内部类的第一个好处就体现出来了 隐藏你不想让别人知道的操作,也即封装性
    同时,我们也发现了在外部类作用范围之外得到内部类对象的第一个方法,那就是利用其外部类的方法创建并返回。上例中的cont()和dest()方法就是这么做的。那么还有没有别的方法呢?当然有,其语法格式如下:
    outerObject=new outerClass(Constructor Parameters);
    outerClass.innerClass innerObject=outerObject.new InnerClass(Constructor Parameters);
    注意在创建非静态内部类对象时,一定要先创建起相应的外部类对象。至于原因,也就引出了我们下一个话题 非静态内部类对象有着指向其外部类对象的引用,对刚才的例子稍作修改:

    public class Goods {
    	private int valueRate = 2;
    	private class Content implements Contents {
    		private int i = 11 * valueRate;
    		public int value() {
    			return i;
    		}
    	}
    	protected class GDestination implements Destination {
    		private String label;
    		private GDestination(String whereTo) {
    			label = whereTo;
    		}
    		public String readLabel() {
    			return label;
    		}
    	}
    	public Destination dest(String s) {
    		return new GDestination(s);
    	}
    	public Contents cont() {
    		return new Content();
    	}
    }
    

    在这里我们给Goods类增加了一个private成员变量valueRate,意义是货物的价值系数,在内部类Content的方法value()计算价值时把它乘上。我们发现,value()可以访问valueRate,这也是内部类的第二个好处 一个内部类对象可以访问创建它的外部类对象的内容,甚至包括私有变量!这是一个非常有用的特性,为我们在设计时提供了更多的思路和捷径。要想实现这个功能,内部类对象就必须有指向外部类对象的引用。Java编译器在创建内部类对象时,隐式的把其外部类对象的引用也传了进去并一直保存着。这样就使得内部类对象始终可以访问其外部类对象,同时这也是为什么在外部类作用范围之外向要创建内部类对象必须先创建其外部类对象的原因。
    有人会问,如果内部类里的一个成员变量与外部类的一个成员变量同名,也即外部类的同名成员变量被屏蔽了,怎么办?没事,Java里用如下格式表达外部类的引用:
    outerClass.this
    有了它,我们就不怕这种屏蔽的情况了。

    静态内部类

    和普通的类一样,内部类也可以有静态的。不过和非静态内部类相比, 区别就在于静态内部类没有了指向外部的引用。

    这实际上和C++中的嵌套类很相像了,Java内部类与C++嵌套类最大的不同就在于是否有指向外部的引用这一点上,当然从设计的角度以及以它一些细节来讲还有区别。
    除此之外,在任何非静态内部类中,都不能有静态数据,静态方法或者又一个静态内部类(内部类的嵌套可以不止一层)。不过静态内部类中却可以拥有这一切。这也算是两者的第二个区别吧。

    局部内部类

    是的,Java内部类也可以是局部的,它可以定义在一个方法甚至一个代码块之内。

    public class Goods1 {
    	public Destination dest(String s) {
    		class GDestination implements Destination {
    			private String label;
    			private GDestination(String whereTo) {
    				label = whereTo;
    			}
    			public String readLabel() {
    				return label;
    			}
    		}
    		return new GDestination(s);
    	}
    	public static void main(String[] args) {
    		Goods1 g = new Goods1();
    		Destination d = g.dest("Beijing");
    	}
    }
    

    上面就是这样一个例子。在方法dest中我们定义了一个内部类,最后由这个方法返回这个内部类的对象。如果我们在用一个内部类的时候仅需要创建它的一个对象并创给外部,就可以这样做。当然,定义在方法中的内部类可以使设计多样化,用途绝不仅仅在这一点。
    下面有一个更怪的例子:

    public class Goods2 {
    	private void internalTracking(boolean b) {
    		if (b) {
    			class TrackingSlip {
    				private String id;
    				TrackingSlip(String s) {
    					id = s;
    				}
    				String getSlip() {
    					return id;
    				}
    			}
    			TrackingSlip ts = new TrackingSlip("slip");
    			String s = ts.getSlip();
    		}
    	}
    	public void track() {
    		internalTracking(true);
    	}
    	public static void main(String[] args) {
    		Goods2 g = new Goods2();
    		g.track();
    	}
    }
    

    你不能在if之外创建这个内部类的对象,因为这已经超出了它的作用域。不过在编译的时候,内部类TrackingSlip和其他类一样同时被编译,只不过它由它自己的作用域,超出了这个范围就无效,除此之外它和其他内部类并没有区别。

    匿名内部类

    java的匿名内部类的语法规则看上去有些古怪,不过如同匿名数组一样,当你只需要创建一个类的对象而且用不上它的名字时,使用内部类可以使代码看上去简洁清楚。它的语法规则是这样的:
    new interfacename(){…}; 或 new superclassname(){…};
    下面接着前面继续举例子:

    public class Goods3 {
    	public Contents cont() {
    		return new Contents() {
    			private int i = 11;
    			public int value() {
    				return i;
    			}
    		};
    	}
    }
    

    这里方法cont()使用匿名内部类直接返回了一个实现了接口Contents的类的对象,看上去的确十分简洁。
    在java的事件处理的匿名适配器中,匿名内部类被大量的使用。例如在想关闭窗口时加上这样一句代码:

    frame.addWindowListener(new WindowAdapter(){
    	public void windowClosing(WindowEvent e){
    	   System.exit(0);
    	}
    }); 
    

    有一点需要注意的是,匿名内部类由于没有名字,所以它没有构造函数(但是如果这个匿名内部类继承了一个只含有带参数构造函数的父类,创建它的时候必须带上这些参数,并在实现的过程中使用super关键字调用相应的内容)。如果你想要初始化它的成员变量,有下面几种方法:
    如果是在一个方法的匿名内部类,可以利用这个方法传进你想要的参数,不过记住, 这些参数必须被声明为final 。

    将匿名内部类改造成有名字的局部内部类,这样它就可以拥有构造函数了。
    在这个匿名内部类中使用初始化代码块。
    为什么需要内部类?
    java内部类有什么好处?为什么需要内部类?
    首先举一个简单的例子,如果你想实现一个接口,但是这个接口中的一个方法和你构想的这个类中的一个方法的名称,参数相同,你应该怎么办?这时候,你可以建一个内部类实现这个接口。由于内部类对外部类的所有内容都是可访问的,所以这样做可以完成所有你直接实现这个接口的功能。
    不过你可能要质疑,更改一下方法的不就行了吗?
    的确,以此作为设计内部类的理由,实在没有说服力。
    真正的原因是这样的,java中的内部类和接口加在一起,可以的解决常被C++程序员抱怨java中存在的一个问题 没有多继承。实际上,C++的多继承设计起来很复杂,而java通过内部类加上接口,可以很好的实现多继承的效果。

    7、Java静态内部类(static class)

    在一个类中创建另外一个类,叫做成员内部类。这个成员内部类可以静态的(利用static关键字修饰),也可以是非静态的。

    一、静态内部类的使用目的。

    在定义内部类的时候,在其前面加上一个权限修饰符static。这个内部类就变为了静态内部类。如在进行代码程序测试的时候,如果在每一个Java源文件中都设置一个主方法(主方法是某个应用程序的入口,必须具有),那么会出现很多额外的代码。而且最主要的是这段主程序的代码对于Java文件来说,只是一个形式,其本身并不需要这种主方法。但是少了这个主方法又是万万不行的。在这种情况下,就可以将主方法写入到静态内部类中,从而不用为每个Java源文件都设置一个类似的主方法。这对于代码测试是非常有用的。在一些中大型的应用程序开发中,则是一个常用的技术手段。

    二、静态内部类的使用限制

    将某个内部类定义为静态类,跟将其他类定义为静态类的方法基本相同,引用规则也基本一致。不过其细节方面仍然有很大的不同。具体来说,主要有如下几个地方要引起各位程序开发人员的注意。

    一是静态成员(包括静态变量与静态成员)的定义。

    在非静态内部类中不可以声明静态成员。如现在在一个student类中定义了一个内部类Age,如果没有将这个类利用static关键字修饰,即没有定义为静态类,那么在这个内部类中如果要利用static关键字来修饰某个成员方法或者成员变量是不允许的。在编译的时候就通不过。故程序开发人员需要注意,只有静态内部类才能够定义静态的成员变量与成员方法。

    二是在成员的引用上,有比较大的限制。

    一般的非静态内部类,可以随意的访问外部类中的成员变量与成员方法。即使这些成员方法被修饰为private(私有的成员变量或者方法)。因为在其他类中是无法访问被定义为私有的成员变量或方法。但是如果一个内部类被定义为静态的,那么在引用外部类的成员方法或者成员变量的时候,就会有诸多的限制。如不能够从静态内部类的对象中访问外部类的非静态成员(包括成员变量与成员方法)。这是什么意思呢?如果在外部类中定义了两个变量,一个是非静态的变量,一个是静态的变量。静态内部类只能引用外部类中的静态的成员(变量或方法),而不能够访问非静态的变量。对于那些非静态的成员变量与成员方法,在静态内部类中是无法访问的。这就是静态内部类的最大使用限制。在普通的非静态内部类中是没有这个限制的。也正是这个原因,决定了静态内部类只应用在一些特定的场合。其应用范围远远没有像非静态的内部类那样广泛。

    三是在创建静态内部类时不需要将静态内部类的实例绑定在外部类的实例上。

    通常情况下,在一个类中创建成员内部类的时候,有一个强制性的规定,即内部类的实例一定要绑定在外部类的实例中。也就是说,在创建内部类之前要先在外部类中要利用new关键字来创建这个内部类的对象。如此的话如果从外部类中初始化一个内部类对象,那么内部类对象就会绑定在外部类对象上。也就是说,普通非静态内部类的对象是依附在外部类对象之中的。通常情况下,程序员在定义静态内部类的时候,是不需要定义绑定在外部类的实例上的。也就是说,要在一个外部类中定义一个静态的内部类,不需要利用关键字new来创建内部类的实例。

    从以上的分析中可以看出,静态内部类与非静态的内部类还是有很大的不同的。一般程序开发人员可以这么理解,非静态的内部类对象隐式地在外部类中保存了一个引用,指向创建它的外部类对象。

    牢记两个差别:

    一、如是否可以创建静态的成员方法与成员变量(静态内部类可以创建静态的成员,而非静态的内部类不可以)

    二、对于访问外部类的成员的限制(静态内部类只可以访问外部类中的静态成员变量与成员方法,而非静态的内部类即可以访问所有的外部类成员方法与成员变量)。

    这两个差异是静态内部类与非静态外部类最大的差异,也是静态内部类之所以存在的原因。了解了这个差异之后,程序开发人员还需要知道,在什么情况下该使用静态内部类。如在程序测试的时候,为了避免在各个Java源文件中书写主方法的代码,可以将主方法写入到静态内部类中,以减少代码的书写量,让代码更加的简洁。

    总 之,静态内部类在Java语言中是一个很特殊的类,跟普通的静态类以及非静态的内部类都有很大的差异。作为程序开发人员,必须要知道他们之间的差异,并在 实际工作中在合适的地方采用合适的类。不过总的来说,静态内部类的使用频率并不是很高。但是在有一些场合,如果没有这个内部静态类的话,可能会起到事倍功半的反面效果。

    非静态内部类实例:

    package common.lang;
    
    public class Student {
    
        private String name;
        private int age;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    
        public class Child{
            private String name1;
            private int age1;
    
            public String getName1() {
                return name1;
            }
            public void setName1(String name1) {
                this.name1 = name1;
            }
            public int getAge1() {
                System.out.println(age);
                return age1;
            }
            public void setAge1(int age1) {
                this.age1 = age1;
            }
    
    
        }
    
        public static void main(String[] args) {
            Student s = new Student();
            s.setAge(12);
            s.setName("yao");
            Child c = s.new Child();
            System.out.println(c.getAge1());
        }
    }
    
    

    静态内部类实例:

    package common.lang;
    
    public class Student {
    
    
        private String name;
        private static int age;
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
        public static int getAge() {
            return age;
        }
    
        public static void setAge(int age) {
            Student.age = age;
        }
    
    
        public static class Child{
            private String name1;
            private int age1;
    
            public String getName1() {
                return name1;
            }
            public void setName1(String name1) {
                this.name1 = name1;
            }
            public int getAge1() {
                System.out.println(age);
                return age1;
            }
            public void setAge1(int age1) {
                this.age1 = age1;
            }
    
    
        }
    
        public static void main(String[] args) {
            Student s = new Student();
            Child c = new Child();
        }
    }
    
    
    展开全文
  • 详解 Java 内部类

    千次阅读 2021-03-06 18:10:33
    内部类Java 里面算是非常常见的一个功能了,在日常开发中我们肯定多多少少都用过,这里总结一下关于 Java内部类的相关知识点和一些使用内部类时需要注意的点。从种类上说,内部类可以分为四类:普通内部类、...

    内部类在 Java 里面算是非常常见的一个功能了,在日常开发中我们肯定多多少少都用过,这里总结一下关于 Java 中内部类的相关知识点和一些使用内部类时需要注意的点。

    从种类上说,内部类可以分为四类:普通内部类、静态内部类、匿名内部类、局部内部类。我们来一个个看:

    普通内部类

    这个是最常见的内部类之一了,其定义也很简单,在一个类里面作为类的一个字段直接定义就可以了,例:

    public class InnerClassTest {

    public class InnerClassA {

    }

    }

    在这里 InnerClassA 类为 InnerClassTest 类的普通内部类,在这种定义方式下,普通内部类对象依赖外部类对象而存在,即在创建一个普通内部类对象时首先需要创建其外部类对象,我们在创建上面代码中的 InnerClassA 对象时先要创建 InnerClassTest 对象,例:

    public class InnerClassTest {

    public int field1 = 1;

    protected int field2 = 2;

    int field3 = 3;

    private int field4 = 4;

    public InnerClassTest() {

    // 在外部类对象内部,直接通过 new InnerClass(); 创建内部类对象

    InnerClassA innerObj = new InnerClassA();

    System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");

    System.out.println("其内部类的 field1 字段的值为: " + innerObj.field1);

    System.out.println("其内部类的 field2 字段的值为: " + innerObj.field2);

    System.out.println("其内部类的 field3 字段的值为: " + innerObj.field3);

    System.out.println("其内部类的 field4 字段的值为: " + innerObj.field4);

    }

    public class InnerClassA {

    public int field1 = 1;

    protected int field2 = 2;

    int field3 = 3;

    private int field4 = 4;

    //        static int field5 = 5; // 编译错误!普通内部类中不能定义 static 属性

    public InnerClassA() {

    System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");

    System.out.println("其外部类的 field1 字段的值为: " + field1);

    System.out.println("其外部类的 field2 字段的值为: " + field2);

    System.out.println("其外部类的 field3 字段的值为: " + field3);

    System.out.println("其外部类的 field4 字段的值为: " + field4);

    }

    }

    public static void main(String[] args) {

    InnerClassTest outerObj = new InnerClassTest();

    // 不在外部类内部,使用:外部类对象. new 内部类构造器(); 的方式创建内部类对象

    //        InnerClassA innerObj = outerObj.new InnerClassA();

    }

    }

    这里的内部类就像外部类声明的一个属性字段一样,因此其的对象时依附于外部类对象而存在的,我们来看一下结果:

    2c2d58ac620e396478f7e4caed6931c9.png

    我们注意到,内部类对象可以访问外部类对象中所有访问权限的字段,同时,外部类对象也可以通过内部类的对象引用来访问内部类中定义的所有访问权限的字段,后面我们将从源码里面分析具体的原因。

    我们下面来看一下静态内部类。

    静态内部类

    我们知道,一个类的静态成员独立于这个类的任何一个对象存在,只要在具有访问权限的地方,我们就可以通过 类名.静态成员名 的形式来访问这个静态成员,同样的,静态内部类也是作为一个外部类的静态成员而存在,创建一个类的静态内部类对象不需要依赖其外部类对象。例:

    public class InnerClassTest {

    public int field1 = 1;

    public InnerClassTest() {

    System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");

    // 创建静态内部类对象

    StaticClass innerObj = new StaticClass();

    System.out.println("其内部类的 field1 字段的值为: " + innerObj.field1);

    System.out.println("其内部类的 field2 字段的值为: " + innerObj.field2);

    System.out.println("其内部类的 field3 字段的值为: " + innerObj.field3);

    System.out.println("其内部类的 field4 字段的值为: " + innerObj.field4);

    }

    static class StaticClass {

    public int field1 = 1;

    protected int field2 = 2;

    int field3 = 3;

    private int field4 = 4;

    // 静态内部类中可以定义 static 属性

    static int field5 = 5;

    public StaticClass() {

    System.out.println("创建 " + StaticClass.class.getSimpleName() + " 对象");

    //            System.out.println("其外部类的 field1 字段的值为: " + field1); // 编译错误!!

    }

    }

    public static void main(String[] args) {

    // 无需依赖外部类对象,直接创建内部类对象

    //        InnerClassTest.StaticClass staticClassObj = new InnerClassTest.StaticClass();

    InnerClassTest outerObj = new InnerClassTest();

    }

    }

    结果:

    5d32bf27f357287be3dbe0e7a97d315d.png

    可以看到,静态内部类就像外部类的一个静态成员一样,创建其对象无需依赖外部类对象(访问一个类的静态成员也无需依赖这个类的对象,因为它是独立于所有类的对象的)。但是于此同时,静态内部类中也无法访问外部类的非静态成员,因为外部类的非静态成员是属于每一个外部类对象的,而本身静态内部类就是独立外部类对象存在的,所以静态内部类不能访问外部类的非静态成员,而外部类依然可以访问静态内部类对象的所有访问权限的成员,这一点和普通内部类无异。

    匿名内部类

    匿名内部类有多种形式,其中最常见的一种形式莫过于在方法参数中新建一个接口对象 / 类对象,并且实现这个接口声明 / 类中原有的方法了:

    public class InnerClassTest {

    public int field1 = 1;

    protected int field2 = 2;

    int field3 = 3;

    private int field4 = 4;

    public InnerClassTest() {

    System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");

    }

    // 自定义接口

    interface OnClickListener {

    void onClick(Object obj);

    }

    private void anonymousClassTest() {

    // 在这个过程中会新建一个匿名内部类对象,

    // 这个匿名内部类实现了 OnClickListener 接口并重写 onClick 方法

    OnClickListener clickListener = new OnClickListener() {

    // 可以在内部类中定义属性,但是只能在当前内部类中使用,

    // 无法在外部类中使用,因为外部类无法获取当前匿名内部类的类名,

    // 也就无法创建匿名内部类的对象

    int field = 1;

    @Override

    public void onClick(Object obj) {

    System.out.println("对象 " + obj + " 被点击");

    System.out.println("其外部类的 field1 字段的值为: " + field1);

    System.out.println("其外部类的 field2 字段的值为: " + field2);

    System.out.println("其外部类的 field3 字段的值为: " + field3);

    System.out.println("其外部类的 field4 字段的值为: " + field4);

    }

    };

    // new Object() 过程会新建一个匿名内部类,继承于 Object 类,

    // 并重写了 toString() 方法

    clickListener.onClick(new Object() {

    @Override

    public String toString() {

    return "obj1";

    }

    });

    }

    public static void main(String[] args) {

    InnerClassTest outObj = new InnerClassTest();

    outObj.anonymousClassTest();

    }

    }

    来看看结果:

    b70bab7679679fe17d6c8f94e9387e84.png

    上面的代码中展示了常见的两种使用匿名内部类的情况:

    直接 new 一个接口,并实现这个接口声明的方法,在这个过程其实会创建一个匿名内部类实现这个接口,并重写接口声明的方法,然后再创建一个这个匿名内部类的对象并赋值给前面的 OnClickListener 类型的引用;

    new 一个已经存在的类 / 抽象类,并且选择性的实现这个类中的一个或者多个非 final 的方法,这个过程会创建一个匿名内部类对象继承对应的类 / 抽象类,并且重写对应的方法。

    同样的,在匿名内部类中可以使用外部类的属性,但是外部类却不能使用匿名内部类中定义的属性,因为是匿名内部类,因此在外部类中无法获取这个类的类名,也就无法得到属性信息。

    局部内部类

    局部内部类使用的比较少,其声明在一个方法体 / 一段代码块的内部,而且不在定义类的定义域之内便无法使用,其提供的功能使用匿名内部类都可以实现,而本身匿名内部类可以写得比它更简洁,因此局部内部类用的比较少。来看一个局部内部类的小例子:

    public class InnerClassTest {

    public int field1 = 1;

    protected int field2 = 2;

    int field3 = 3;

    private int field4 = 4;

    public InnerClassTest() {

    System.out.println("创建 " + this.getClass().getSimpleName() + " 对象");

    }

    private void localInnerClassTest() {

    // 局部内部类 A,只能在当前方法中使用

    class A {

    // static int field = 1; // 编译错误!局部内部类中不能定义 static 字段

    public A() {

    System.out.println("创建 " + A.class.getSimpleName() + " 对象");

    System.out.println("其外部类的 field1 字段的值为: " + field1);

    System.out.println("其外部类的 field2 字段的值为: " + field2);

    System.out.println("其外部类的 field3 字段的值为: " + field3);

    System.out.println("其外部类的 field4 字段的值为: " + field4);

    }

    }

    A a = new A();

    if (true) {

    // 局部内部类 B,只能在当前代码块中使用

    class B {

    public B() {

    System.out.println("创建 " + B.class.getSimpleName() + " 对象");

    System.out.println("其外部类的 field1 字段的值为: " + field1);

    System.out.println("其外部类的 field2 字段的值为: " + field2);

    System.out.println("其外部类的 field3 字段的值为: " + field3);

    System.out.println("其外部类的 field4 字段的值为: " + field4);

    }

    }

    B b = new B();

    }

    //        B b1 = new B(); // 编译错误!不在类 B 的定义域内,找不到类 B,

    }

    public static void main(String[] args) {

    InnerClassTest outObj = new InnerClassTest();

    outObj.localInnerClassTest();

    }

    }

    同样的,在局部内部类里面可以访问外部类对象的所有访问权限的字段,而外部类却不能访问局部内部类中定义的字段,因为局部内部类的定义只在其特定的方法体 / 代码块中有效,一旦出了这个定义域,那么其定义就失效了,就像代码注释中描述的那样,即外部类不能获取局部内部类的对象,因而无法访问局部内部类的字段。最后看看运行结果:

    585b22d4371880ec74d2456d82e2e95c.png

    内部类的嵌套

    内部类的嵌套,即为内部类中再定义内部类,这个问题从内部类的分类角度去考虑比较合适:

    普通内部类:在这里我们可以把它看成一个外部类的普通成员方法,在其内部可以定义普通内部类(嵌套的普通内部类),但是无法定义 static 修饰的内部类,就像你无法在成员方法中定义 static 类型的变量一样,当然也可以定义匿名内部类和局部内部类;

    静态内部类:因为这个类独立于外部类对象而存在,我们完全可以将其拿出来,去掉修饰它的 static 关键字,他就是一个完整的类,因此在静态内部类内部可以定义普通内部类,也可以定义静态内部类,同时也可以定义 static 成员;

    匿名内部类:和普通内部类一样,定义的普通内部类只能在这个匿名内部类中使用,定义的局部内部类只能在对应定义域内使用;

    局部内部类:和匿名内部类一样,但是嵌套定义的内部类只能在对应定义域内使用。

    深入理解内部类

    不知道小伙伴们对上面的代码有没有产生疑惑:非静态内部类可以访问外部类所有访问权限修饰的字段(即包括了 private 权限的),同时,外部类也可以访问内部类的所有访问权限修饰的字段。而我们知道,private 权限的字段只能被当前类本身访问。然而在上面我们确实在代码中直接访问了对应外部类 / 内部类的 private 权限的字段,要解除这个疑惑,只能从编译出来的类下手了,为了简便,这里采用下面的代码进行测试:

    public class InnerClassTest {

    int field1 = 1;

    private int field2 = 2;

    public InnerClassTest() {

    InnerClassA inner = new InnerClassA();

    int v = inner.x2;

    }

    public class InnerClassA {

    int x1 = field1;

    private int x2 = field2;

    }

    }

    我在外部类中定义了一个默认访问权限(同一个包内的类可以访问)的字段 field1, 和一个 private 权限的字段 field2 ,并且定义了一个内部类 InnerClassA ,并且在这个内部类中也同样定义了两个和外部类中定义的相同修饰权限的字段,并且访问了外部类对应的字段。最后在外部类的构造方法中我定义了一个方法内变量赋值为内部类中 private 权限的字段。我们用 javac 命令(javac InnerClassTest.java)编译这个 .java 文件,会得到两个 .classs 文件。InnerClassTest.class 和 InnerClassTest$InnerClassA.class,我们再用 javap -c 命令(javap -c InnerClassTest 和 javap -c InnerClassTest$InnerClassA)分别反编译这两个 .class 文件,InnerClassTest.class 的字节码如下:

    0536fbd5a3007a497d7f5aeeed304d59.png

    我们注意到字节码中多了一个默认修饰权限并且名为 access$100 的静态方法,其接受一个 InnerClassTest 类型的参数,即其接受一个外部类对象作为参数,方法内部用三条指令取到参数对象的 field2 字段的值并返回。由此,我们现在大概能猜到内部类对象是怎么取到外部类的 private 权限的字段了:就是通过这个外部类提供的静态方法。

    类似的,我们注意到 24 行字节码指令 invokestatic ,这里代表执行了一个静态方法,而后面的注释也写的很清楚,调用的是 InnerClassTest$InnerClassA.access$000 方法,即调用了内部类中名为 access$000 的静态方法,根据我们上面的外部类字节码规律,我们也能猜到这个方法就是内部类编译过程中编译器自动生成的,那么我们赶紧来看一下 InnerClassTest$InnerClassA 类的字节码吧:

    703e267a92cae5e46b9d559db93e06b7.png

    果然,我们在这里发现了名为 access$000 的静态方法,并且这个静态方法接受一个 InnerClassTest$InnerClassA 类型的参数,方法的作用也很简单:返回参数代表的内部类对象的 x2 字段值。

    我们还注意到编译器给内部类提供了一个接受 InnerClassTest 类型对象(即外部类对象)的构造方法,内部类本身还定义了一个名为 this$0 的 InnerClassTest 类型的引用,这个引用在构造方法中指向了参数所对应的外部类对象。

    最后,我们在 25 行字节码指令发现:内部类的构造方法通过 invokestatic 指令执行外部类的 access$100 静态方法(在 InnerClassTest 的字节码中已经介绍了)得到外部类对象的 field2 字段的值,并且在后面赋值给 x2 字段。这样的话内部类就成功的通过外部类提供的静态方法得到了对应外部类对象的 field2 。

    上面我们只是对普通内部类进行了分析,但其实匿名内部类和局部内部类的原理和普通内部类是类似的,只是在访问上有些不同:外部类无法访问匿名内部类和局部内部类对象的字段,因为外部类根本就不知道匿名内部类 / 局部内部类的类型信息(匿名内部类的类名被隐匿,局部内部类只能在定义域内使用)。但是匿名内部类和局部内部类却可以访问外部类的私有成员,原理也是通过外部类提供的静态方法来得到对应外部类对象的私有成员的值。而对于静态内部类来说,因为其实独立于外部类对象而存在,因此编译器不会为静态内部类对象提供外部类对象的引用,因为静态内部类对象的创建根本不需要外部类对象支持。但是外部类对象还是可以访问静态内部类对象的私有成员,因为外部类可以知道静态内部类的类型信息,即可以得到静态内部类的对象,那么就可以通过静态内部类提供的静态方法来获得对应的私有成员值。来看一个简单的代码证明:

    public class InnerClassTest {

    int field1 = 1;

    private int field2 = 2;

    public InnerClassTest() {

    InnerClassA inner = new InnerClassA();

    int v = inner.x2;

    }

    // 这里改成了静态内部类,因而不能访问外部类的非静态成员

    public static class InnerClassA {

    private int x2 = 0;

    }

    }

    同样的编译步骤,得到了两个 .class 文件,这里看一下内部类的 .class 文件反编译的字节码 InnerClassTest$InnerClassA:

    cc910d8a657110ab1b7b485178a577bb.png

    仔细看一下,确实没有找到指向外部类对象的引用,编译器只为这个静态内部类提供了一个无参构造方法。

    而且因为外部类对象需要访问当前类的私有成员,编译器给这个静态内部类生成了一个名为 access$000 的静态方法,作用已不用我多说了。如果我们不看类名,这个类完全可以作为一个普通的外部类来看,这正是静态内部类和其余的内部类的区别所在:静态内部类对象不依赖其外部类对象存在,而其余的内部类对象必须依赖其外部类对象而存在。

    OK,到这里问题都得到了解释:在非静态内部类访问外部类私有成员 / 外部类访问内部类私有成员 的时候,对应的外部类 / 外部类会生成一个静态方法,用来返回对应私有成员的值,而对应外部类对象 / 内部类对象通过调用其内部类 / 外部类提供的静态方法来获取对应的私有成员的值。

    内部类和多重继承

    我们已经知道,Java 中的类不允许多重继承,也就是说 Java 中的类只能有一个直接父类,而 Java 本身提供了内部类的机制,这是否可以在一定程度上弥补 Java 不允许多重继承的缺陷呢?我们这样来思考这个问题:假设我们有三个基类分别为 A、B、C,我们希望有一个类 D 达成这样的功能:通过这个 D 类的对象,可以同时产生 A 、B 、C 类的对象,通过刚刚的内部类的介绍,我们也应该想到了怎么完成这个需求了,创建一个类 D.java:

    class A {}

    class B {}

    class C {}

    public class D extends A {

    // 内部类,继承 B 类

    class InnerClassB extends B {

    }

    // 内部类,继承 C 类

    class InnerClassC extends C {

    }

    // 生成一个 B 类对象

    public B makeB() {

    return new InnerClassB();

    }

    // 生成一个 C 类对象

    public C makeC() {

    return new InnerClassC();

    }

    public static void testA(A a) {

    // ...

    }

    public static void testB(B b) {

    // ...

    }

    public static void testC(C c) {

    // ...

    }

    public static void main(String[] args) {

    D d = new D();

    testA(d);

    testB(d.makeB());

    testC(d.makeC());

    }

    }

    程序正确运行。而且因为普通内部类可以访问外部类的所有成员并且外部类也可以访问普通内部类的所有成员,因此这种方式在某种程度上可以说是 Java 多重继承的一种实现机制。但是这种方法也是有一定代价的,首先这种结构在一定程度上破坏了类结构,一般来说,建议一个 .java 文件只包含一个类,除非两个类之间有非常明确的依赖关系(比如说某种汽车和其专用型号的轮子),或者说一个类本来就是为了辅助另一个类而存在的(比如说上篇文章介绍的 HashMap 类和其内部用于遍历其元素的 HashIterator 类),那么这个时候使用内部类会有较好代码结构和实现效果。而在其他情况,将类分开写会有较好的代码可读性和代码维护性。

    内部类和内存泄露

    在这一小节开始前介绍一下什么是内存泄露:即指在内存中存在一些其内存空间可以被回收的对象因为某些原因又没有被回收,因此产生了内存泄露,如果应用程序频繁发生内存泄露可能会产生很严重的后果(内存中可用的空间不足导致程序崩溃,甚至导致整个系统卡死)。

    听起来怪吓人的,这个问题在一些需要开发者手动申请和释放内存的编程语言(C/C++)中会比较容易产生,因为开发者申请的内存需要手动释放,如果忘记了就会导致内存泄露,举个简单的例子(C++):

    #include 

    int main() {

    // 申请一段内存,空间为 100 个 int 元素所占的字节数

    int *p = new int[100];

    // C++ 11

    p = nullptr;

    return 0;

    }

    在这段代码里我有意而为之:在为指针 p 申请完内存之后将其直接赋值为 nullptr ,这是 C++ 11 中一个表示空指针的关键字,我们平时常用的 NULL 只是一个值为 0 的常量值,在进行方法重载传参的时候可能会引起混淆。之后我直接返回了,虽然在程序结束之后操作系统会回收我们程序中申请的内存,但是不可否认的是上面的代码确实产生了内存泄露(申请的 100 个 int 元素所占的内存无法被回收)。这只是一个最简单不过的例子。我们在写这类程序的时候当动态申请的内存不再使用时,应该要主动释放申请的内存:

    #include 

    int main() {

    // 申请一段内存,空间为 100 个 int 元素所占的字节数

    int *p = new int[100];

    // 释放 p 指针所指向的内存空间

    delete[] p;

    // C++ 11

    p = nullptr;

    return 0;

    }

    而在 Java 中,因为 JVM 有垃圾回收功能,对于我们自己创建的对象无需手动回收这些对象的内存空间,这种机制确实在一定程度上减轻了开发者的负担,但是也增加了开发者对 JVM 垃圾回收机制的依赖性,从某个方面来说,也是弱化了开发者防止内存泄露的意识。当然,JVM 的垃圾回收机制的利是远远大于弊的,只是我们在开发过程中不应该丧失了这种对象和内存的意识。

    回到正题,内部类和内存泄露又有什么关系呢?在继续阅读之前,请确保你对 JVM 的在进行垃圾回收时如何找出内存中不再需要的对象有一定的了解,如果你对这个过程不太了解,你可以参考一下 这篇文章 中对这个过程的简单介绍。我们在上面已经知道了,创建非静态内部类的对象时,新建的非静态内部类对象会持有对外部类对象的引用,这个我们在上面的源码反编译中已经介绍过了,正是因为非静态内部类对象会持有外部类对象的引用,因此如果说这个非静态内部类对象因为某些原因无法被回收,就会导致这个外部类对象也无法被回收,这个听起来是有道理的,因为我们在上文也已经介绍了:非静态内部类对象依赖于外部类对象而存在,所以内部类对象没被回收,其外部类对象自然也不能被回收。但是可能存在这种情况:非静态内部类对象在某个时刻已经不在被使用,或者说这个内部类对象可以在不影响程序正确运行的情况下被回收,而因为我们对这个内部类的使用不当而使得其无法被 JVM 回收,同时会导致其外部类对象无法被回收,即为发生内存泄露。那么这个 “使用不当” 具体指的是哪个方面呢?看一个简单的例子,新建一个 MemoryLeakTest 的类:

    public class MemoryLeakTest {

    // 抽象类,模拟一些组件的基类

    abstract static class Component {

    final void create() {

    onCreate();

    }

    final void destroy() {

    onDestroy();

    }

    // 子类实现,模拟组件创建的过程

    abstract void onCreate();

    // 子类实现,模拟组件摧毁的过程

    abstract void onDestroy();

    }

    // 具体某个组件

    static class MyComponent extends Component {

    // 组件中窗口的单击事件监听器

    static OnClickListener clickListener;

    // 模拟组件中的窗口

    MyWindow myWindow;

    @Override

    void onCreate() {

    // 执行组件内一些资源初始化的代码

    clickListener = new OnClickListener() {

    @Override

    public void onClick(Object obj) {

    System.out.println("对象 " + obj + " 被单击");

    }

    };

    // 新建我的窗口对象,并设置其单击事件监听器

    myWindow = new MyWindow();

    myWindow.setClickListener(clickListener);

    }

    @Override

    void onDestroy() {

    // 执行组件内一些资源回收的代码

    myWindow.removeClickListener();

    }

    }

    // 我的窗口类,模拟一个可视化控件

    static class MyWindow {

    OnClickListener clickListener;

    // 设置当前控件的单击事件监听器

    void setClickListener(OnClickListener clickListener) {

    this.clickListener = clickListener;

    }

    // 移除当前控件的单击事件监听器

    void removeClickListener() {

    this.clickListener = null;

    }

    }

    // 对象的单击事件的监听接口

    public interface OnClickListener {

    void onClick(Object obj);

    }

    public static void main(String[] args) {

    MyComponent myComponent = new MyComponent();

    myComponent.create();

    myComponent.destroy();

    // myComponent 引用置为 null,排除它的干扰

    myComponent = null;

    // 调用 JVM 的垃圾回收动作,回收无用对象

    System.gc();

    System.out.println("");

    }

    }

    我们在代码中添加一些断点,然后采用 debug 模式查看:

    ca4e738bdf06a487f69e01df6a9aa0b9.png

    程序执行到 72 行代码,此时 72 行代码还未执行,因此 myComponent 引用和其对象还未创建,继续执行:

    e3b22d959981e90b683cf92f7581522c.png

    这里成功创建了一个 MyComponent 对象,但是其 create 方法还未执行,所以 myWindow 字段为 null,这里可能有小伙伴会问了,myComponent 对象的 clickListener 字段呢?怎么不见了?其实这和我们在代码中定义 clickListener 字段的形式有关,我们定义的是 static OnClickListener clickListener; ,因此 clickListener 是一个静态字段,其在类加载的完成的时候储存在 JVM 中内存区域的 方法区 中,而创建的 Java 对象储存在 JVM 的堆内存中,两者不在同一块内存区域。关于这些细节,想深入了解的小伙伴建议阅读《深入理解JVM虚拟机》。好了,我们继续执行代码:

    13d6856e38645a15f3de483d31fb5d11.png

    myComponent.create 方法执行完成之后创建了 OnClickListener 内部类对象,并且为 myWindow 对象设置 OnCLickListener 单击事件监听。我们继续:

    a773b0fd12a1e56645ae6100d78e82ee.png

    myComponent.destroy 方法执行完成之后,myWindow.removeClickListener 方法也执行完成,此时 myWindow 对象中的 clickListener字段为 null。我们继续:

    c77f768f9b694ae7ba6c0aa7e8d1c929.png

    代码执行到了 80 行,在此之前,所有的代码和解释都没有什么难度,跟着运行图走,一切都那么顺利成章,其实这张图的运行结果也很好理解,只不过图中的文字需要思考一下:myComponent 引用指向的对象真的被回收了吗?要解答这个问题,我们需要借助 Java 中提供的内存分析工具 jvisualvm (以前它还不叫这个名字…),它一般在你安装 JDK 的目录下的 bin 子目录下:

    5ae9d9ca57aa4445b6910b4d5015ff45.png

    我们运行这个程序:

    e743560a0bfe456f756fb3622c7856d3.png

    在程序左边可以找到我们当前正在执行的 Java 进程,双击进入:

    30bdca25e88dddd665240d30eab58b0f.png

    单击 tab 中的 监视 选项卡,可以看到当前正在执行的 Java 进程的一些资源占用信息,当然我们现在的主要目的是分析内存,那么们单击右上角的 堆 Dump :

    198397ce1aaa7106ea5ff0e811d04d11.png

    在这个界面,单击 类 选项卡,会出现当前 Java 进程中用到的所有的类,我们已经知道我们要查找的类的对象只创建了一个,因此我们根据右上角的 实例数 来进行排除:我们成功的找到了我们创建的对象!而这样也意味着当我们在上面代码中调用 JVM 的垃圾回收动作没有回收这三个对象,这其实就是一个真真切切的内存泄露!因为我们将 main 方法中的 myComponent 引用赋值为 null,就意味着我们已经不再使用这个组件和里面的一些子组件(MyWindow 对象),即这个组件和其内部的一些组件应该被回收。但是调用 JVM 的垃圾回收却并没有将其对应的对象回收。造成这个问题的原因在哪呢?

    其实就在于我们刚刚在 MyComponent 类中定义的 clickListener 字段,我们在代码中将其定义成了 static 类型的,同时这个字段又指向了一个匿名内部类对象(在 create 方法中 创建了一个 OnClickListener 接口对象,即通过一个匿名内部类实现这个接口并创建其对象),根据 JVM 寻找和标记无用对象的规则(可达性分析算法),其会将 clickListener 字段作为一个 “root” ,并通过它来寻找还有用的对象,在这个例子中,clickListener 字段指向一个匿名内部类对象,这个匿名内部类对象有一个外部类对象(MyComponent 类型的对象)的引用,而外部类对象中又有一个 MyWindow 类型的对象引用。因此 JVM 会将这三个对象都视为有用的对象不会回收。用图来解释吧:

    a5979db4dbf3bebfda98d5adf914b591.png

    Ok,通过这个过程,相信你已经理解了造成此次内存泄露的原因了,那么我们该如何解决呢?对于当前这个例子,我们只需要改一些代码:

    把 MyComponent 类中的 clickListener 字段前面的 static 修饰符去掉就可以了(static OnClickListener clickListener; -> OnClickListener clickListener;),这样的话 clickListener 指向的对象,就作为 MyComponent 类的对象的一部分了,在 MyComponent 对象被回收时里面的子组件也会被回收。同时它们之间也只是互相引用(MyComponent 外部类对象中有一个指向 OnClickListener 内部类对象的引用,OnClickListener 内部类对象有一个指向 MyComponent 外部类对象的引用),根据 JVM 的 “可达性分析” 算法,在两个对象都不再被外部使用时,JVM 的垃圾回收机制是可以标记并回收这两个对象的。 虽然不强制要求你在 MyComponent 类中的 onDestroy 方法中将其 clickListener 引用赋值为 null,但是我还是建议你这样做,因为这样更能确保你的程序的安全性(减少发生内存泄露的机率,毕竟匿名内部类对象会持有外部类对象的引用),在某个组件被销毁时将其内部的一些子组件进行合理的处理是一个很好的习惯。

    你也可以自定义一个静态内部类或者是另外自定义一个类文件,并实现 OnClickListener 接口,之后通过这个类创建对象,这样就可以避免通过非静态内部类的形式创建 OnClickListener 对象增加内存泄露的可能性。

    避免内存泄漏

    那么我们在日常开发中怎么合理的使用内部类来避免产生内存泄露呢?这里给出一点我个人的理解:

    能用静态内部类就尽量使用静态内部类,从上文中我们也知道了,静态内部类的对象创建不依赖外部类对象,即静态内部对象不会持有外部类对象的引用,自然不会因为静态内部类对象而导致内存泄露,所以如果你的内部类中不需要访问外部类中的一些非 static 成员,那么请把这个内部类改造成静态内部类;

    对于一些自定义类的对象,慎用 static 关键字修饰(除非这个类的对象的声明周期确实应该很长),我们已经知道,JVM 在进行垃圾回收时会将 static 关键字修饰的一些静态字段作为 “root” 来进行存活对象的查找,所以程序中 static 修饰的对象越多,对应的 “root” 也就越多,每一次 JVM 能回收的对象就越少。 当然这并不是建议你不使用 static 关键字,只是在使用这个关键字之前可以考虑一下这个对象使用 static 关键字修饰对程序的执行确实更有利吗?

    为某些组件(大型)提供一个当这个大型组件需要被回收的时候用于合理处理其中的一些小组件的方法(例如上面代码中 MyComponent 的 onDestroy 方法),在这个方法中,确保正确的处理一些需要处理的对象(将某些引用置为 null、释放一些其他(CPU…)资源)

    展开全文
  • java内部类的定义、内部类的分类

    千次阅读 2021-04-24 10:05:33
    内部类 基本介绍 一个类的内部又完整的嵌套了另一个类结构。被嵌套的类称为内部类(inner class),嵌套其他类的类称为外部类(outer class)。是我们类的第五大成员,内部类最大的特点就是可以直接访问...1)成员内部类(没
  • JAVA创建内部类对象

    千次阅读 2020-04-11 18:13:34
    //对于成员内部类,必须先产生外部类的实例化对象,才能产生内部类的实例化对象 //创建成员内部类对象的一般形式为: //外部类类名.内部类类名 xxx = 外部类对象名.new 内部类类名() //T...
  • Java内部类使用注意事项

    千次阅读 2021-03-15 03:10:48
    非静态内部类成员可以访问外部类实例成员 (如注释1),但外部类访问非静态内部类成员 必须创建非静态内部类对象来访问其成员,如注释2public class Lab02 {private int ss=5;private class InnerClass{private int ...
  • 主要介绍了Java内部类应用之静态内部类应用,结合实例形式分析了Java静态内部类的原理、功能、用法及相关操作注意事项,需要的朋友可以参考下
  • main方法访问普通内部类的例子: //外部类test public class Test { //非静态的外部类成员变量 int num=1; /** * 普通内部类Person * */ class Person{ int age; } public static void main(String...
  • 根据成员内部类的定义: 首先生成外部类的实例对象 然后生成绑定到外部类实例对象的成员内部类实例对象 外部类实例对象的生成一定要先于成员内部类实例对象的生成 public class InnerClassDemo{ //对于final修饰...
  • 静态内部类内部类的关系 只有内部类才能被声明为静态类,即...如果一个内部类不是被定义成静态内部类,那么在定义成员变量或者成员方法的时候,是不能够被定义成静态的; 总结: 是否能拥有静态成员:静态内部类.
  • 内部类中不能使用静态的变量和静态的方法,这句话今天刚听到的时候感觉没毛病,但是,内部类中也不一定不能使用静态的变量,我们可以通过添加final 的方法的来使用,内部类中静态的方法是不能使用的 看下面的代码 ...
  • java内部类之成员内部类、局部内部类和匿名内部类

    千次阅读 多人点赞 2018-07-15 16:13:30
    1、成员内部类。 2、局部内部类(包含匿名内部类)。 成员内部类 定义格式: 修饰符 class 类名称 { 修饰符 class 类名称 { //... } //... } 注意: 内部类使用外部,可以随意访问,但是外部类使用内部类...
  • Java 内部类

    千次阅读 2021-04-03 08:35:05
    主要用以下几种成员内部类局部内部类匿名内部类静态内部类Java 成员内部类示例访问权限private 访问权限protected 访问权限public 访问权限package 访问权限public class TestClass {public void a(){LogHelper....
  • 内部类简介 如何创建内部类(Java)

    千次阅读 2022-04-03 16:49:19
    在《Think in java》中有这样一句话:使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。 在我们程序设计中有...
  • 02:外部类 访问 内部类 的成员 需要 创建 内部类的 对象,之后 可以 访问 内部类 的 任何 成员,包 括private成员,需要注意的是成员内部类不可以有静态成员。 03:当外部类 的成员 和 内部类的 成员重名时单单用...
  • 成员内部类 局部内部类(包含匿名内部类) 成员内部类 定义格式: 修饰符 class 外部类名称 { 修饰符 class 内部类名称 { // .... } // .... } 内部类的class文件命名为:外部类$内部类.class 使用成员内部类 ...
  • Java 内部类详解

    千次阅读 2021-03-06 17:54:43
    什么定义在一个类内部的类,称为内部类(累不累),如下:public ...}}}C称为A的内部类,简称内部类A称为C的外部类,简称外部类而且内部类能访问外部类的成员(静态成员、实例成员),当然有一些限制,限制如下4种声明方...
  • java内部类怎么写

    千次阅读 2021-02-12 21:36:40
    一。内部类基础二。深入理解内部类三。内部类的使用场景和好处四。常见的与内部类相关的笔试面试题若有不正之处,请多谅解并欢迎批评指正。...1.成员内部类成员内部类是最普通的内部类,它的定义...
  • Java 静态内部类 的详细解释

    千次阅读 2021-03-11 02:30:47
    01:静态内部类成员可以是 静态的数据域 和 静态的方法 ,因为这个类是静态的, 在加载的时候一并被加载进去如下图: //解释: 这个Outer在被虚拟机加载的时候, name被初始化, 因为StaticInner 依然是一个静态的...
  • Java内部类以及使用场景

    千次阅读 2020-12-15 19:14:36
    1. 它体现了一种代码的隐藏机制和访问控制机制,内部类与所在外部类有一定的关系,往往只有该外部类调用此内部类,所以没有必要专门用一个Java文件存放这个类。 public class Outer {  private int num;  ...
  • java普通内部类和静态内部类的区别

    千次阅读 2022-03-12 17:41:19
    java普通内部类和静态内部类的区别
  • * 标题:Java内部类[成员内部类、局部内部类、匿名内部类和静态内部类] * * 作者:Nstar * * 时间:2020年3月13日 * * 内容: * * 问:什么是内部类? * 答:在 Java 中,可以将一个类定义在另一个类里面或者一...
  • 内部类的分类 成员内部类 局部内部类 静态内部类 匿名内部类(GUI中用到 1.1. 2. 成员内部类 内部类和外部类的实例变量可以共存 在内部类中访问实例变量:this.属性 在内部类访问外部类的实例变量:外部类名.this.属性...
  • Java内部类是如何调用外部类的成员变量和方法的?我们下看看下面的例子 package com.iworktool.libjava; public class Outer{ private String privateValue = "privateValue"; public String publicValue = ...
  • java中的内部类与使用场景

    千次阅读 2022-04-01 22:48:12
    java中的内部类java中的内部类成员内部类静态内部类局部内部类匿名内部类 java中的内部类 java内部类分为:成员内部类、静态内部类、局部内部类、匿名内部类。 使用场景: 成员内部类:不想被其他类公开使用的类,...
  • Java内部类的定义

    千次阅读 多人点赞 2021-11-06 19:42:42
    目录 一、前言 二、内部类 语法格式: static修饰内部类 内部类未被static修饰时: ...内部类被static修饰后 ...前面我们学习了一个小的项目,现在我们来学习新的知识点,... 外部类成员要访问内部类是,必须创建内部类
  • java内部类 和外部类的区别

    千次阅读 2021-03-07 00:47:13
    下面说一说内部类(Inner Class)和静态内部类(Static Nested Class)的区别:定义在一个类内部的类叫内部类,包含内部类的类称为外部类。内部类可以声明public、protected、private等访问限制,可以声明 为abstract的...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 367,321
精华内容 146,928
关键字:

java成员内部类