Java 内部类

版权声明:本文为CSDN博主「奋斗的bigHead」的原创文章,遵循CC 4.0 BY-SA版权协议
原文链接:https://blog.csdn.net/u013728021/article/details/87358517

1.什么是内部类

定义在类中的类

2.内部类的作用

我们为什么需要内部类?或者说内部类为啥要存在?其主要原因有如下几点:

  • 内部类方法可以访问该类定义所在作用域中的数据,包括被 private 修饰的私有数据
  • 内部类可以对同一包中的其他类隐藏起来
  • 内部类可以解决 java 单继承的缺陷
  • 当我们想要定义一个回调函数却不想写大量代码的时候我们可以选择使用匿名内部类来实现

2.1 可以无条件地访问外围类(外部类)的所有元素

为什么可以引用:

内部类虽然和外部类写在同一个文件中, 但是编译完成后, 还是生成各自的class文件,内部类通过this访问外部类的成员。

1 编译器自动为内部类添加一个成员变量, 这个成员变量的类型和外部类的类型相同, 这个成员变量就是指向外部类对象(this)的引用;

2 编译器自动为内部类的构造方法添加一个参数, 参数的类型是外部类的类型, 在构造方法内部使用这个参数为内部类中添加的成员变量赋值;

3 在调用内部类的构造函数初始化内部类对象时,会默认传入外部类的引用。

编译指令 javac classpath(.java文件的路径)
反编译指令 javap -v(详细信息) classpath(.class文件的路径)

/**
 * 内部类无条件访问外部类元素
 */
public class DataOuterClass {
    private String data = "外部类数据";
    
    private class InnerClass {
        public InnerClass() {
            System.out.println(data);
        }
    }

    public void getInner() {
        new InnerClass();
    }

    public static void main(String[] args) {
        DataOuterClass outerClass = new DataOuterClass();
        outerClass.getInner();
    }
}

打印

外部类数据

data这是在DataOuterClass定义的私有变量。这个变量在内部类中可以无条件地访问.

2.2 实现隐藏

关于内部类的第二个好处其实很显而易见,我们都知道外部类即普通的类不能使用 private protected 访问权限符来修饰的,而内部类则可以使用 private 和 protected 来修饰。当我们使用 private 来修饰内部类的时候这个类就对外隐藏了。这看起来没什么作用,但是当内部类实现某个接口的时候,在进行向上转型,对外部来说,就完全隐藏了接口的实现了

接口

public interface InnerInterface {
    void innerMethod();
}

具体类

/**
 * 实现信息隐藏
 */
public class OuterClass {

    /**
     * private修饰内部类,实现信息隐藏
     */
    private class InnerClass implements InnerInterface {
        @Override
        public void innerMethod() {
            System.out.println("实现内部类隐藏");
        }
    }

    public InnerInterface getInner() {
        return new InnerClass();
    }
}

调用程序

public class Test {
    public static void main(String[] args) {
        OuterClass outerClass = new OuterClass();
        InnerInterface inner = outerClass.getInner();
        inner.innerMethod();
    }
}

打印

实现内部类隐藏

从这段代码里面我只知道OuterClass的getInner()方法能返回一个InnerInterface接口实例但我并不知道这个实例是怎么实现的。

而且由于InnerClass是private的,所以我们如果不看代码的话根本看不到这个具体类的名字,所以说它可以很好的实现隐藏。

2.3 可以实现多重继承

我们知道 java 是不允许使用 extends 去继承多个类的。内部类的引入可以很好的解决这个事情。
我的理解 Java只能继承一个类这个学过基本语法的人都知道,而在有内部类之前它的多重继承方式是用接口来实现的。但使用接口有时候有很多不方便的地方。比如我们实现一个接口就必须实现它里面的所有方法。
而有了内部类就不一样了。它可以使我们的类继承多个具体类或抽象类。如下面这个例子:

类一

public class ExampleOne {
    public String name() {
        return "inner";
    }
}

类二

public class ExampleTwo {
    public int age() {
        return 25;
    }
}

类三

public class MainExample {
   /**
    * 内部类1继承ExampleOne
    */
   private class InnerOne extends ExampleOne {
       public String name() {
           return super.name();
       }
   }

   /**
    * 内部类2继承ExampleTwo
    */
   private class InnerTwo extends ExampleTwo {
       public int age() {
           return super.age();
       }
   }

   public String name() {
       return new InnerOne().name();
   }

   public int age() {
       return new InnerTwo().age();
   }

   public static void main(String[] args) {
       MainExample mi = new MainExample();
       System.out.println("姓名:" + mi.name());
       System.out.println("年龄:" + mi.age());
   }
}

大家注意看类三,里面分别实现了两个内部类 InnerOne和InnerTwo ,InnerOne类又继承了ExampleOne,InnerTwo继承了ExampleTwo,这样我们的类三MainExample就拥有了ExampleOne和ExampleTwo的方法和属性,也就间接地实现了多继承。

2.4 通过匿名内部类来优化简单的接口实现

关于匿名内部类相信大家都不陌生,我们常见的点击事件的写法就是这样的:
不用再去实现OnClickListener对象了。

...
    view.setOnClickListener(new View.OnClickListener(){
        @Override
        public void onClick(){
            // ... do XXX...
        }
    })
...

3.内部类与外部类的关系

  • 对于非静态内部类,内部类的创建依赖外部类的实例对象,在没有外部类实例之前是无法创建内部类的
  • 内部类是一个相对独立的实体,与外部类不是is-a关系
  • 创建内部类的时刻并不依赖于外部类的创建

对于普通内部类创建方法有两种:

public class ClassOuter {
    
    public void fun(){
        System.out.println("外部类方法");
    }
    
    public class InnerClass{
       
    }
}

public class TestInnerClass {
    public static void main(String[] args) {
        //创建方式1
        ClassOuter.InnerClass innerClass = new ClassOuter().new InnerClass();
        //创建方式2
        ClassOuter outer = new ClassOuter();
        ClassOuter.InnerClass inner = outer.new InnerClass();
    }
}

3.1 内部类的分类

内部类可以分为:静态内部类(嵌套类)和非静态内部类。非静态内部类又可以分为:成员内部类、方法内部类、匿名内部类。

静态内部类和非静态内部类的区别

静态内部类 非静态内部类
是否可以有静态成员变量
是否可以访问外部类的非静态变量
是否可以访问外部类的静态变量
创建是否依赖于外部类

我们通过一个例子就可以很好的理解这几点区别:

public class ClassOuter {
    private int noStaticInt = 1;
    private static int STATIC_INT = 2;

    public void fun() {
        System.out.println("外部类方法");
    }

    public class InnerClass {
        //static int num = 1; 此时编辑器会报错 非静态内部类则不能有静态成员
        public void fun(){
            //非静态内部类的非静态成员可以访问外部类的非静态变量。
            System.out.println(STATIC_INT);
            System.out.println(noStaticInt);
        }
    }

    public static class StaticInnerClass {
        static int NUM = 1;//静态内部类可以有静态成员
        public void fun(){
            System.out.println(STATIC_INT);
            //System.out.println(noStaticInt); 此时编辑器会报 不可访问外部类的非静态变量错
        }
    }
}

public class TestInnerClass {
    public static void main(String[] args) {
        //非静态内部类 创建方式1
        ClassOuter.InnerClass innerClass = new ClassOuter().new InnerClass();
        //非静态内部类 创建方式2
        ClassOuter outer = new ClassOuter();
        ClassOuter.InnerClass inner = outer.new InnerClass();
        //静态内部类的创建方式
        ClassOuter.StaticInnerClass staticInnerClass = new ClassOuter.StaticInnerClass();
    }
}

局部内部类

定义

如果一个内部类只在一个方法中使用到了,那么我们可以将这个类定义在方法内部,这种内部类被称为局部内部类。其作用域仅限于该方法。

局部内部类有两点值得我们注意的地方:

  • 局部内类不允许使用访问权限修饰符 public private protected 均不允许
  • 局部内部类对外完全隐藏,除了创建这个类的方法可以访问它其他的地方是不允许访问的。
  • 局部内部类与成员内部类不同之处是他可以引用成员变量,但该成员必须声明为 final,并内部不允许修改该变量的值。(这句话并不准确,因为如果不是基本数据类型的时候,只是不允许修改引用指向的对象,而对象本身是可以被就修改的)
public class ClassOuter {
    private int noStaticInt = 1;
    private static int STATIC_INT = 2;

    public void fun() {
        System.out.println("外部类方法");
    }
    
    public void testFunctionClass(){
        class FunctionClass{
            private void fun(){
                System.out.println("局部内部类的输出");
                System.out.println(STATIC_INT);
                System.out.println(noStaticInt);
                System.out.println(params);
                //params ++ ; // params 不可变所以这句话编译错误
            }
        }
        FunctionClass functionClass = new FunctionClass();
        functionClass.fun();
    }
}

匿名内部类

  • 匿名内部类是没有访问修饰符的。
  • 匿名内部类必须继承一个抽象类或者实现一个接口
  • 匿名内部类中不能存在任何静态成员或方法
  • 匿名内部类是没有构造方法的,因为它没有类名。
  • 与局部内部类相同匿名内部类也可以引用局部变量。此变量也必须声明为 final
public class Button {
    public void click(final int params){
        //匿名内部类,实现的是ActionListener接口
        new ActionListener(){
            public void onAction(){
                System.out.println("click action..." + params);
            }
        }.onAction();
    }
    //匿名内部类必须继承或实现一个已有的接口
    public interface ActionListener{
        public void onAction();
    }

    public static void main(String[] args) {
        Button button=new Button();
        button.click();
    }
}

为什么局部变量需要final修饰呢

因为局部变量和匿名内部类的生命周期不同。

匿名内部类是创建后是存储在堆中的,而方法中的局部变量是存储在Java栈中,当方法执行完毕后,就进行退栈,同时局部变量也会消失。

那么此时匿名内部类还有可能在堆中存储着,那么匿名内部类要到哪里去找这个局部变量呢?

为了解决这个问题编译器为自动地帮我们在匿名内部类中创建了一个局部变量的备份,也就是说即使方法执结束,匿名内部类中还有一个备份,自然就不怕找不到了。

但是问题又来了。

如果局部变量中的a不停的在变化。
那么岂不是也要让备份的a变量无时无刻的变化。
为了保持局部变量与匿名内部类中备份域保持一致。
编译器不得不规定死这些局部域必须是常量,一旦赋值不能再发生变化了。

所以为什么匿名内部类应用外部方法的域必须是常量域的原因所在了。

特别注意
在Java8中已经去掉要对final的修饰限制,但其实只要在匿名内部类使用了,该变量还是会自动变为final类型(只能使用,不能赋值)。

4.实际开发中内部类有可能会引起的问题

内部类会造成程序的内存泄漏
相信做 Android 的朋友看到这个例子一定不会陌生,我们经常使用的 Handler 就无时无刻不给我们提示着这样的警告。

我们先来看下内部类为什么会造成内存泄漏。

要想了解为啥内部类为什么会造成内存泄漏我们就必须了解 java 虚拟机的回收机制,但是我们这里不会详尽的介绍 java 的内存回收机制,我们只需要了解 java 的内存回收机制通过「可达性分析」来实现的。

即 java 虚拟机会通过内存回收机制来判定引用是否可达,如果不可达就会在某些时刻去回收这些引用。

那么内部类在什么情况下会造成内存泄漏的可能呢?

如果一个匿名内部类没有被任何引用持有,那么匿名内部类对象用完就有机会被回收。

如果内部类仅仅只是在外部类中被引用,当外部类的不再被引用时,外部类和内部类就可以都被GC回收。

如果当内部类的引用被外部类以外的其他类引用时,就会造成内部类和外部类无法被GC回收的情况,即使外部类没有被引用,因为内部类持有指向外部类的引用)。

public class ClassOuter {

    Object object = new Object() {
        public void finalize() {
            System.out.println("inner Free the occupied memory...");
        }
    };

    public void finalize() {
        System.out.println("Outer Free the occupied memory...");
    }
}

public class TestInnerClass {
    public static void main(String[] args) {
        try {
            Test();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private static void Test() throws InterruptedException {
        System.out.println("Start of program.");

        ClassOuter outer = new ClassOuter();
        Object object = outer.object;
        outer = null;

        System.out.println("Execute GC");
        System.gc();

        Thread.sleep(3000);
        System.out.println("End of program.");
    }
}

运行程序发现 执行内存回收并没回收 object 对象,
这是因为即使外部类没有被任何变量引用,只要其内部类被外部类以外的变量持有,外部类就不会被GC回收。

我们要尤其注意内部类被外面其他类引用的情况,这点导致外部类无法被释放,极容易导致内存泄漏。

在Android 中 Hanlder 作为内部类使用的时候其对象被系统主线程的 Looper (当然这里也可是子线程手动创建的 Looper)掌管的消息队列 MessageQueue 中的 Hanlder 发送的 Message 持有,当消息队列中有大量消息处理的需要处理,或者延迟消息需要执行的时候,创建该 Handler 的 Activity 已经退出了,Activity 对象也无法被释放,这就造成了内存泄漏。
那么 Hanlder 何时会被释放,当消息队列处理完 Hanlder 携带的 message 的时候就会调用 msg.recycleUnchecked()释放Message所持有的Handler引用。

在 Android 中要想处理 Hanlder 内存泄漏可以从两个方面着手:

在关闭Activity/Fragment 的 onDestry,取消还在排队的Message:

mHandler.removeCallbacksAndMessages(null);

将 Hanlder 创建为静态内部类并采用软引用方式

mHandler
   private static class MyHandler extends Handler {

        private final WeakReference<MainActivity> mActivity;

        public MyHandler(MainActivity activity) {
            mActivity = new WeakReference<MainActivity>(activity);
        }

        @Override
        public void handleMessage(Message msg) {
            MainActivity activity = mActivity.get();
            if (activity == null || activity.isFinishing()) {
               return;
            }
            // ...
        }
    }
posted @ 2021-12-24 09:57  镇魂帆-张  阅读(80)  评论(0编辑  收藏  举报