java面试(6): 内部类相关面试知识详细整理

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/yz_cfm/article/details/85330942

                                                  

一、什么是内部类?

    可以将一个类的定义放在另一个类的定义内部,这就是内部类。
 

二、为什么要存在内部类?

    a. 内部类方法可以访问该类定义所在的作用域中的数据(也就是该内部类的外围类),包括外围类中 private 修饰的私有成员变量和方法。
    b. 内部类可以对同一包中的其它类隐藏起来。
    c. 内部类可以实现 java 单继承的缺陷。
    d. 当我们想要定义一个回调函数却又不想写大量代码的时候我们可以选择使用匿名内部类来实现。

    a 论证:  

class MainActivity extends AppCompatActivity {
       ...
       private List<Fragment> fragments = new ArrayList();

       private class BottomPagerAdapter extends FragmentPagerAdapter {
             ...
        @Override
        public Fragment getItem(int position) { return fragments.get(position);  }
             ...
       }
       ...
}

    如上代码,我们做Android的人应该都十分熟悉。这里的 BottomPagerAdapter 就是 MainActivity 中的一个内部类。我们可以看到 BottomPagerAdapter 可以直接访问 MainActivity 中定义的 fragments 私有变量。如果BottomPagerAdapter 是一个外部类,那么访问 MainActivity 中的私有变量就要借助 getxxx() 方法了,这就是内部类的第一点好处。

    为什么内部类可以随意访问外部类的成员呢?
    答:当外部类的对象创建了一个内部类的对象时,内部类对象必定会秘密捕获一个指向外部类对象的引用,然后内部类就可以通过这个应用访问外部类的成员和方法,这些都是编译器帮我们处理的。

    另外注意内部类只是一种编译器现象,与虚拟机无关。编译器会将内部类编译成 外部类名$内部类名 的常规文件,虚拟机对此一无所知。

    b 论证:

public interface Incrementable {
       void increment();
}

public class Example {
       private class InsideClass implements InterfaceTest {
             public void test() {
                    System.out.println("这是一个测试");
             }
       }

       public InterfaceTest getIn() {
             return new InsideClass();
       }

}

public class TestExample {
       public static void main(String args[]) {
             Example a = new Example();
             InterfaceTest a1 = a.getIn();
             a1.test();
       }
}

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

    如上面的代码示例,从这段代码里面我只知道 Example 的 etIn() 方法能返回一个 InterfaceTest 实例但我并不知道这个实例是这么实现的。而且由于 InsideClass 是 private 的,所以我们如果不看代码的话根本看不到这个具体类的名字,所以说它可以很好的实现隐藏。

    c 论证:

    我们知道 java 是不允许使用 extends 去继承多个类的。内部类的引入可以很好的解决这个事情。 以下引用 《Thinking In Java》中的一段话:

    每个内部类都可以对应的继承自一个(接口的)实现,所以无论外围类是否已经继承了某个(接口的)实现,对于内部类没有影响 如果没有内部类提供的、可以继承多个具体的或抽象的类的能力,一些设计与编程问题就难以解决。从这个角度看,内部类使得多重继承的解决方案变得完整。接口解决了部分问题,而内部类有效的实现了"多重继承"。也就是说,内部类允许继承多个非接口类型(类或抽象类)。

下面看两个例子来说明通过接口实现 "多继承" 和通过内部类实现 "多继承"的区别:

    eg1(当实现两个接口类型的类时候,通过接口和内部类实现 "多继承" 的效果一样):
 

interface A{
       void A();
}

interface B{
       void B();
}

class C implements A,B{
       @Override
       public void B() {
             System.out.println("C中实现接口B");
       }

       @Override
       public void A() {
             System.out.println("C中实现接口A");
       }
}

class D implements A{
       @Override
       public void A() {
             System.out.println("D中实现接口A");
       }

       public B makeB() {
             return new B() {
                    @Override
                    public void B() {
                           System.out.println("D中实现接口B");
                    }};
       }
}

public class Test {
       static void testMethod1(A a) { a.A(); };
       static void testMethod2(B b) { b.B(); };

       public static void main(String[] args) {
             // 类 C 通过实现多个接口的方式实现 "多继承"
             C c = new C();
             testMethod1(c);
             testMethod2(c);
             
             System.out.println("-------------------------------------");

             // 类 D 通过内部类的方式实现 "多继承"
             D d = new D();
             testMethod1(d);
             testMethod2(d.makeB());
       }
}



output:
C中实现接口A
C中实现接口B
-------------------------------------
D中实现接口A
D中实现接口B

    eg2(当实现两个非接口类型类(也就是普通类和抽象类)的时候,只能通过内部类实现 "多继承"):

class A{
       void methodA() {}
}

abstract class B{ abstract void methodB(); }


class C extends A{
       @Override
       void methodA() {
             System.out.println("类C中的methodA方法");
       }

       B makeB() {
             return new B() {
                    @Override
                    void methodB() {
                           System.out.println("类C中的methodB方法");
                    }};
       }
}



public class Test {
       static void testMethod1(A a) { a.methodA(); }
       static void testMethod2(B b) { b.methodB(); }

       public static void main(String[] args) {
             C c = new C();
             testMethod1(c);
             testMethod2(c.makeB());
       }
}

output:
类C中的methodA方法
类C中的methodB方法

综上可知: 内部类可以实现多接口中无法实现的 "多继承" 问题。

    d 论证:

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

这里就通过匿名内部类的方式大大减少了代码量,一般匿名内部类常用于定义的类只使用一次的情况下。

三、内部类与外部类之间的关系

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


    这里解释一下上面说的 “创建内部类的时刻并不依赖于外部类的创建”,这是《Thinking in java》中的一句话,很多人认为它和第一条矛盾,其实不然。这句话的重点是在 "时刻" 二字。作者其实是想突出内部类是外部类的 "轻量级的可选组件" 这个特性。举个例子: 迭代器( Iterator )作为很多容器的内部类,并不是在创建容器的时候就被一起被创建的,而是在我们需要的时候手动创建它的实例对象。

创建静态和非静态内部类对象方法:

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

    public class InnerClass{
        
    }

    public static class StaticInnerClass{

    }
}

public class TestInnerClass {

    public static void main(String[] args) {
        // 创建方式非静态内部类实例对象
        ClassOuter.InnerClass innerClass = new ClassOuter().new InnerClass();

        // 或者如下:
        ClassOuter outer = new ClassOuter();
        ClassOuter.InnerClass inner = outer.new InnerClass();

        // 创建静态内部类实例对象
        ClassOuter.StaticInnerClass staticInnerIns = new  ClassOuter.StaticInnerClass();
    }
}

    值得注意的是:正是由于这种依赖关系,所以普通内部类中不允许有 static 成员,包括嵌套类(内部类的静态内部类),道理显而易知:static 本身是针对类本身来说的。又由于非static内部类总是由一个外部类的对象创建,既然与对象相关,就不能有静态的字段和方法。当然静态内部类不依赖于外部类,所以其内允许有 static 成员。

四、 内部类的分类及其几种分类的详细使用和注意事项

    内部类可以分为:静态内部类(嵌套类)和非静态内部类。
    非静态内部类又可以分为:成员内部类、方法内部类、匿名内部类。对于这几种类的书写相信大家早已熟练,所以这里主要说明的是这几种类之间的区别。
    静态内部类和非静态内部类的区别:
    1. 静态内部类可以有静态成员,而非静态内部类则不能有静态成员。
    2. 静态内部类可以访问外部类的静态变量,而不可访问外部类的非静态变量;
    3. 非静态内部类的非静态成员可以访问外部类的非静态变量。
    4. 静态内部类的创建不依赖于外部类,而非静态内部类必须依赖于外部类的创建而创建。

    上面这个区别应该很好理解,就不详细用代码演示了。

    局部内部类:
    如果一个内部类只在某一个方法中使用到,那么我们可以将这个类定义在方法内部,这种内部类被称为局部内部类。其作用域仅限于该方法。
    局部内部类中值得我们注意的地方:
    1. 局部内部类不允许使用访问权限修饰符 public 、private 、protected 均不允许
    2. 局部内部类对外完全隐藏,除了创建这个类的方法可以访问它,其他的地方是不允许访问的
    3. 局部内部类与成员内部类不同之处是他可以引用成员变量,但该成员必须声明为 final,并内部不允许修改该变量的值。(这里我们需要注意,它只是不允许修改指向对象的引用,而不是引用所指向的对象本身,其实对象本身是可以被就修改的)

五、实际开发中使用内部类遇到的问题分析

    相信做 Android 的朋友看到这个例子一定不会陌生,我们经常使用的 Handler 就无时无刻不给我们提示着这样的警告---(内存泄漏)。我们先来看下内部类为什么会造成内存泄漏。
    要想了解为什么内部类会造成内存泄漏我们就必须了解 java 虚拟机的内存回收机制,但是我们这里不会详尽的介绍 java 的内存回收机制,我们只需要了解 java 的内存回收机制通过「可达性分析」来实现的。即 java 虚拟机会通过内存回收机制来判定该引用是否可达,如果不可达就会在某些时刻去回收这些引用。

    那么内部类在什么情况下会造成内存泄漏的可能呢?
    * 如果一个匿名内部类没有被任何引用持有,那么匿名内部类对象用完就有机会被回收。
    * 如果内部类仅仅只是在外部类中被引用,当外部类对象不再被引用时,外部类和内部类就可以都被GC回收。
    * 如果当内部类的引用被外部类以外的其他类引用时,就会造成内部类和外部类无法被GC回收的情况,虽然此时外部类没有被引用,但因为内部类持有指向外部类的引用。

这里看两种情况:
1. 

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.");
    }
}

output:
Start of program.
Execute GC
inner Free the occupied memory...
Outer Free the occupied memory...
End of program.

2.

去掉 1 中的注释,运行结果如下:
Start of program.
Execute GC
End of program.

结果分析:
    运行程序 2 后发现执行内存回收时并没回收 object 对象和其外部类对象,这是因为即使外部类没有被任何变量引用,只要其内部类被外部类以外的变量持有,外部类就不会被GC回收。我们要尤其注意内部类被外面其他类引用的情况,这点导致外部类无法被释放,极容易导致内存泄漏。

    回到开始说的 Android 中使用Handler所引起的内存泄漏问题:

    1. 为什么会引起内存泄漏?

    简单的说,因为在java中非静态内部类都会隐式的持有当前类的外部引用,由于 Handler 是 Activity 中的非静态内部类,所以它持有对当前 Activity 的隐式应用。首先我们需要知道主程序的 Looper 的程序和应用程序的生命周期一致,而 Handler 对象是通过主线程中的 looper 最终处理消息,在我们使用 Handler 时我们知道,消息是通过子线程中 mHhandler.sendEmptyMessage(message) 将 message 对象发送到由主线程 Looper 持有的消息队列 MessageQueue 中并最终由该 Handler 对象处理,当消息队列中有大量消息需要处理,或者延迟消息需要执行的时候,那么此时虽然创建该 Handler 的 Activity 已经退出了但由于 Looper 还持有对该 handler 的引用,所以就导致 Activity 对象无法被释放,这就造成了内存泄漏。那么 Hanlder 何时会被释放,当消息队列处理完 Hanlder 携带的 message 的时候就会调用 msg.recycleUnchecked()释放Message所持有的Handler引用。

    2. 解决方案
a. 
    在关闭Activity/Fragment 的 onDestry,取消还在排队的Message:

mHandler.removeCallbacksAndMessages(null);

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

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;
            }
            // ...
        }
    }

全文参考: https://juejin.im/post/5a903ef96fb9a063435ef0c8
 

猜你喜欢

转载自blog.csdn.net/yz_cfm/article/details/85330942