一次搞定内部类

目录

介绍

effective final 例子:

变量引用的例子:

为什么内部类调用的外部变量必须是final修饰的

为什么要使用内部类

什么情况下使用局部内部类

内部类加载时机

非静态内部类的引用导致的内存泄漏问题

介绍

内部类,也叫嵌套类(Nested Classes),主要包含:成员内部类、局部内部类、匿名内部类和静态内部类。

Java的内部类也是一个语法糖,它仅仅是一个编译时的概念,outer.java里面定义了一个内部类inner,一旦编译成功,就会生成两个完全不同的.class文件了,分别是outer.class和outer$inner.class。所以内部类的名字完全可以和它的外部类名字相同。

这里注意静态内部类的.class没有outer$的引用。

静态和非静态的区别:

1.非静态内部类能够访问外部类的静态和非静态成员。静态类不能访问外部类的非静态成员。他只能访问外部类的静态成员。

2.内部静态类不需要有指向外部类的引用。但非静态内部类需要持有对外部类的引用。

 

特点

是否允许private 与protected修饰类

能否存在静态变量

能否定义接口

能否访问外部类方法的参数

能否访问外部类的非静态参数

什么情况下被用到

成员内部类

跟成员变量一样的位置

是(匿名内部类除外),这也是内部类的作用之一,起到了更好的封装性

否, 由于非静态内部类不能脱离外围类实例单独存在,所以它不能有static成员. static成员是和类相关的, 不和实例相关,但是可以定义static final 的常量。

其实归根结底,还是类与对象的区别,静态属性不依赖于对象,因为它保存在jvm的静态区,所以访问修改的时候不需要依赖当前有没有存活的对象,在虚拟机加载的时候也是优先于实例生成的。而实例对象则是保存在jvm的堆内存中,想要访问内部类,必须先实例化外部类,然后通过外部类才能访问内部类。内部类其实也可以认为是外部类的一个成员变量,只要是成员变量,各个对象都是不依赖的,静态属性的出现破坏了这一逻辑,所以java语言在语义层面不允许我们那么做,这其实不是技术问题,是一个语言的逻辑和语义问题。

 

否, 接口是隐式static的

是,甚至私有变量也可以访问

 

看下面为什么要使用内部类的四点,都适用

局部内部类

外部类方法内定义局部类

是,因为定义在方法内部,但必须是final修饰的局部变量,注意java 8 引入了effective final的概念,可以不用手动加final

角度一:如果我们在用一个内部类的时候仅需要创建它的一个对象并创给外部,就可以这样做。

角度二:
有这样一种内部类,它是嵌套在方法和作用域内的,对于这个类的使用主要是应用与解决比较复杂的问题,想创建一个类来辅助我们的解决方案,到那时又不希望这个类是公共可用的,所以就产生了局部内部类,局部内部类和成员内部类一样被编译,只是它的作用域发生了改变,它只能在该方法和属性中被使用,出了该方法和属性就会失效。

匿名内部类

外部类方法内定义匿名局部类

角度一:匿名内部类也可以说是局部内部类的一种,有时候一个类只使用一次,就可以用匿名内部类,告诉GC只用一次就可以回收了,同时也可以简化代码和方便地定义回调。

角度二:

  • 只用到类的一个实例。

  • 类在定义后马上用到。

  • 类非常小(SUN推荐是在4行代码以下)

  • 给类命名并不会导致你的代码更容易被理解。

静态内部类

如果不需要内部类对象与其外围类对象之间有联系,那么可以将内部类声明为static

否,可以跟外部类看作是两个类的关系

不能够从静态内部类的对象中访问外部类的非静态成员(包括成员变量与成员方法)。

一般针对于静态内部类不需要外部类的资源,而外部类需要使用内部类的时候。(这句写的太好了,感觉说到了本质,胜过了太多文章的废话!)

应用比如: 单例模式,builder模式

 

effective final 例子:

代码块

public class EffectivelyFinalDemo 
{
  public static void main(String[] args) 
  {
      //局部内部类和匿名内部类访问的局部变量必须由final修饰,java8开始,可以不加final修饰符,由系统默认添加
     //因此下面两句的效果是一样的
    //final int age=99;
    int age=99;
    //运行代码 <1>将会抛出以下错误
    //EffectivelyFinalDemo.java:14: 错误: 从内部类引用的本地变量必须是最终变量或实际上
        //的最终变量
    //age=11;  <1>
    A a=new A()
    {
      public void test()
      {
        //Cannot refer to a non-final variable age 
        //inside an inner class defined in a different method
        System.out.println(age);
      }
    };
    a.test();
  }
}
//接口
interface A
{
  void test();
}

还需要了解

变量引用的例子:

代码块

public class ShadowTest {
    public int x = 0;
    class FirstLevel {
        public int x = 1;
        void methodInFirstLevel(int x) {
            System.out.println("x = " + x);
            System.out.println("this.x = " + this.x);
            System.out.println("ShadowTest.this.x = " + ShadowTest.this.x);
        }
    }
    public static void main(String... args) {
        ShadowTest st = new ShadowTest();
        ShadowTest.FirstLevel fl = st.new FirstLevel();
        fl.methodInFirstLevel(23);
    }
}

输出结果:

x = 23
this.x = 1
ShadowTest.this.x = 0

为什么内部类调用的外部变量必须是final修饰的

回答一:因为生命周期的原因。方法中的局部变量,方法结束后这个变量就要释放掉,final保证这个变量始终指向一个对象。首先,内部类和外部类其实是处于同一个级别,内部类不会因为定义在方法中就会随着方法的执行完毕而跟随者被销毁。问题就来了,如果外部类的方法中的变量不定义final,那么当外部类方法执行完毕的时候,这个局部变量肯定也就被GC了,然而内部类的某个方法还没有执行完,这个时候他所引用的外部变量已经找不到了。如果定义为final,java会将这个变量复制一份作为成员变量内置于内部类中,这样的话,由于final所修饰的值始终无法改变,所以这个变量所指向的内存区域就不会变。 为了解决:局部变量的生命周期与局部内部类的对象的生命周期的不一致性问题

回答二:看stackoverflow上的一个讨论 http://stackoverflow.com/questions/3910324/why-java-inner-classes-require-final-outer-instance-variables

stackoverflow里最高票的答案说到,当主方法结束时,局部变量会被cleaned up 而内部类可能还在运行。当局部变量声明为final时,当使用已被cleaned up的局部变量时会把局部变量替换成常量:

The compiler can then just replace the use of lastPrice and price in the anonymous class with the values of the constants (at compile time, ofcourse)

也就是说当变量是final时,编译器会将final局部变量"复制"一份,复制品直接作为局部内部中的数据成员.这样,当局部内部类访问局部变量时,其实真正访问的是这个局部变量的"复制品"。因此:当运行栈中的真正的局部变量死亡时,局部内部类对象仍可以访问局部变量(其实访问的是"复制品")。而且,由于被final修饰的变量赋值后不能再修改,所以就保证了复制品与原始变量的一致。给人的感觉:好像是局部变量的"生命期"延长了。这就是java的闭包。

而java8的lambda 表达式之所以不用写final,是因为Java8这里加了一个语法糖:在lambda表达式以及匿名类内部,如果引用某局部变量,则直接将其视为final。本质并没有改变。

 

为什么要使用内部类

原因主要有以下四点:
1.内部类可以很好的实现隐藏

 一般的非内部类,是不允许有 private 与protected权限的,但内部类可以

2.内部类拥有外围类的所有元素的访问权限

3.可是实现多重继承

4.可以避免修改接口而实现同一个类中两种同名方法的调用。

其中最主要的是第三点,实现多继承,

在《Think in java》中有这样一句话:使用内部类最吸引人的原因是:每个内部类都能独立地继承一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类都没有影响。

因为Java不支持多继承,支持实现多个接口。但有时候会存在一些使用接口很难解决的问题,这个时候我们可以利用内部类提供的、可以继承多个具体的或者抽象的类的能力来解决这些程序设计问题。可以这样说,接口只是解决了部分问题,而内部类使得多重继承的解决方案变得更加完整。

 

大家都知道Java只能继承一个类,它的多重继承在我们没有学习内部类之前是用接口来实现的。但使用接口有时候有很多不方便的地方。比如我们实现一个接口就必须实现它里面的所有方法。而有了内部类就不一样了。它可以使我们的类继承多个具体类或抽象类。

必须有个例子(成员局部类为例):

代码块   类1

 package insidecategory;
public class Example1 {
   public String name()
   {
       return "liutao";
   }
}

代码块  类2

package insidecategory;
public class Example2 {
    public int age()
    {
        return 25;
    }
}

 

代码块  类3

package insidecategory;
public class MainExample
{
   private class test1 extends Example1
    {
        public String name()
        {
          return super.name();
        }
    }
    private class test2 extends Example2
    {
       public int age()
       {
         return super.age();
       }
    }
    public String name()
    {
    return new test1().name();
   }
   public int age()
   {
       return new test2().age();
   }
   public static void main(String args[])
   {
       MainExample mi=new MainExample();
       System.out.println("姓名:"+mi.name());
       System.out.println("年龄:"+mi.age());
   }
}

注意看类3,里面分别实现了两个内部类 test1,和test2 ,test1类又继承了Example1,test2继承了Example2,这样我们的类三MainExample就拥有了Example1和Example2的方法和属性,也就间接地实现了多继承。

 

什么情况下使用局部内部类

如果我们在用一个内部类的时候仅需要创建它的一个对象并创给外部,就可以这样做。

看一个例子:

代码块1

public interface Destination {
String readLabel();
}

 

代码块2

局部内部类 
public class Goods1 {
   public Destination dest(String s) {//dest中我们定义了一个内部类
      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");}
} 

内部类加载时机

  • 内部类和静态内部类都是延时加载的,也就是说只有在明确用到内部类时才加载。只使用外部类时不加载。

  • 外部类初次加载,会初始化静态变量、静态代码块、静态方法,但不会加载内部类和静态内部类。

  • 实例化外部类,调用外部类的静态方法、静态变量,则外部类必须先进行加载,但只加载一次。

非静态内部类的引用导致的内存泄漏问题

非静态内部类会持有外部类的引用,如果内部类的生命周期比外部类的生命周期长,则会造成外部类对象无法销毁,导致内存泄漏,典型的就是android中handler的使用,handler初始化是一个内部类,持有acitivity的引用,会造成内存泄漏。

参考:https://blog.csdn.net/zhrx26/article/details/51152342

参考:
https://blog.csdn.net/mid120/article/details/53644539
https://www.jianshu.com/p/f55b11a4cec2

https://blog.csdn.net/tianxiaoqi2008/article/details/7266264

https://blog.csdn.net/hikvision_java_gyh/article/details/8963562
https://www.cnblogs.com/dolphin0520/p/3811445.html https://blog.csdn.net/u010454030/article/details/80548732
https://blog.csdn.net/shaw1994/article/details/48378471

 

非静态内部类的引用导致的内存泄漏问题

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值