Java 内部类

1 、说点闲话

  因为个人原因,布衣博主的技术博文大半年来一直没时间更新(WHAT ? 这是啥理由),恍恍惚惚间,一年又是头,还是得跳出来,给自己一个交代。

  编程日久,项目开发中最常用的技能大概就是 Ctrl+C  再 Ctrl+V 了,实实在在的代码搬运工。这很正常,人越来越堕,能用现成有的东西绝不会自己造;尤其上班这种食人俸禄的差事,要讲究效率和产出,拷贝修改,虽然低级,却似乎是最高效的做法。不过长此以往的,从码农自身来看,对于自己能力的提升,技能体系的构建,肯定是弊大于利的。所以,有时间还是要沉下来,看书,钻研,学习新东西,与时同步。最近在看《Java 编程思想(第四版)》这本书,对博主这种对Java初通皮毛的人来说,能同时兼顾基础和深度,受益颇多,也是对自己已有的Java知识进行了比较系统的梳理。当然,技术最重要的在于交流分享,所以,读书和实践过程中布衣博主也会有一些自己的心得体会的分享出来,算是抛砖引玉吧。这篇先讲讲Java内部类相关的东西。都是结合书本内化而来的,有知识点,也有布衣博主自己的料。

2、 认识内部类

内部类,顾名思义,就是一个定义在已有的Java类内部的类,就像下面这样:

public class Outer {  //外部类
    class Inner{    //内部类
        public void test(){
            System.out.println("内部类方法");
        }
    }
}

  然后呢?没有然后了,就先这样简洁直观的认识它吧,相信我,内部类本来就很简单,别搞得太复杂了。

  3 、内部类对象创建和基本使用

内部类既然是个Java类,自然是可以创建对象的。当然,这话不准确,往深了说,内部类也可以是抽象的,这肯定是无法创建对象的——不过你如果要这样死究的话,我觉得就有点太囿于语法了,对实际运用帮助不大。简单点的掌握内部类常见的普通情况:普通内部类、静态内部类以及匿名内部类足够了。下面分别介绍——

  普通内部类

 普通情况,或者说最典型的情况,就是一个Java类嵌在另一个Java类中,形成了内、外的格局;外部类就是我们普通的类,内部类也是普通的类,特性都满足Java类的特性,没什么特别的。唯一要说的,就是内部类对象的创建依赖于外部类对象。这很好理解,如果在外部类都没有创建对象的基础上,内部类创建的对象依附于哪个实体呢?也可以说是皮之不存毛将焉附?所以,内部类的对象创建是这样的:

public class InnerTest {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.test(); //有了内部类对象你就可以像正常的类对象一样进行方法调用的骚操作了。
    }
}

 不过,通常情况下,也不会这样去创建内部类对象,基于Java封装的特性,内部类作为一种封装的体现,通常会对调用者隐藏实现细节,而通过外部类的方法来返回内部类的对象实例。普通内部类虽然和外部类是两个类,但是因为处于一个类的内部,还是有些不一样的特性——它可以访问外部类的所以成员,包括私有的字段、方法等,这是因为内部类中隐含的持有对外部类的引用,通过这个引用,就能调用外部类的所有成员了。这有点类似于局部和全局的关系,内部类处于局部,能够直接调用作为全局的外部类的其它成员。说到这里,有个误区需要注意。经常看网上说法就说内部类对象可以访问外部类的成员,好像我创建的内部类对象可以直接调用外部类的成员一样,博主也差点就信了,代码尝试,大靠一声,怎么能访问的?所以,我觉得,技术人员对文字的理解还是有些不够严谨的地方,包括书本上的说法,或因为翻译的原因,一些表达上并不是那么贴切的,这个只能自己多去思辨实践了。这里,我们先给外部类定义方法、字段等来完善一下:

public class Outer {
    private String str;

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

    class Inner {
        public void test() {
            buyi();            // ①
            str = "陈本布衣";  // ②
            System.out.println("内部类方法");
        }
    }
}
public class InnerTest {
    public static void main(String[] args) {
        Outer outer = new Outer();
        Outer.Inner inner = outer.new Inner();
        inner.test(); 
        inner.str;       // ③
        buyi.buyi();     // ④
    }
}

  上面代码中,① ②才是上文中说的,内部类可以访问外部类的所有成员的情况,③和④ 你会发现,是无法编译通过的。也就是说,正确的理解应该是内部类的内部可以通过隐含的外部类的引用去访问外部类的所有成员,而不是内部类对象可以访问外部类的成员,这是有本质区别的,当然你也不能拿着内部类对象的引用去访问外部类的成员,要搞清楚,这是毕竟是两个类,类的封装性决定了,一个类的对象是不能去访问另一个类对象的非静态成员的。

  以上所见都是看起来很正常的内部类,比较普通,其实内部类还有很复杂的情况,比如定义在方法内部、代码块内部的局部内部类,这里就不深究了。我觉得,作为基础性掌握,不必太追求全面了,先入门,用精,当你技术积累够了,很多东西也就通了。

   静态内部类

  Java中的静态,指全局的,比如静态方法、成员变量等,如果访问权限允许,你在任何地方都能都直接使用。未了解内部类之前,不知道你有没有想过,类可不可以也是静态呢?是的,类也可以是静态的,不过必须是内部类才行。静态内部类,也叫嵌套类,Java中标准类库中就有很多现成的应用,比如整形的包装器中用静态内部类来缓存[-128,127]之间的整数。嵌套类因为是静态的,很自然的要从静态方面去考虑与普通内部类的区别。这里用博主的理解来阐述一遍:

  ① 类似于静态方法中你不能使用this关键字,因而嵌套类就失去了普通内部类中那个隐含对外部类的引用this,结果就是你不能在嵌套类中随意访问外部类的非静态成员了;

  ② 静态属性决定了,嵌套类更加独立于外部类,要创建嵌套类的对象完全不依赖外部类的对象实体;

  ③ 静态属性决定了,它会在外部类加载的时候初始化。。。等等,博主差写高兴了,想当然的东西到底靠不靠谱呢?实践是永远是检验真理的唯一标准,上代码——

public class Outer {
    Outer() {
        System.out.println("默认构造器");
    }

    static {
        System.out.println("外部静态块");
    }

    static void outerstatic() {
        System.out.println("外部静态方法");
    }

    static class Inner {
        public static  String str = "陈本布衣";

        static {
            System.out.println("嵌套类静态代码块");
        }

        static void innerstatic() {
            System.out.println("嵌套类静态方法");
        }

        public void test() {
            System.out.println("嵌套类非静态方法");
        }
    }
}
public class InnerTest {
    public static void main(String[] args) {
        Outer outer = new Outer();                       // ①
        Outer.Inner inner = new Outer.Inner();           // ②
        Outer.Inner.innerstatic();                       // ③
        String s = Outer.Inner.str;                      // ④
    }
}

  稍微有点Java基础的都清楚,静态成员是一个类中最先被初始化的部分,所以,如果我们只通过 ① 创建外部类的对象,那么Outer类中的静态代码块肯定会执行,控制台有相应的打印,那静态内部类会不会也被初始化呢? 测试结果是不会,这时候静态类有点像静态方法,你主动叫它,它就静静的待在哪里,不为所动! 单独执行 ② 你会发现,外部类静态块没有初始化,也即是静态内部类独立于外部类,它的初始化不会对外部类有任何影响;执行 ③ ④ 同样的,你会发现,只有在要使用类的内部属性的时候,代码块才会初始化,同样的初始化对外部类的初始化没有产生影响,就像外部类完全不知道内部类在干什么一样。

  这个时候我们再去看包装器中关于缓存的静态内部类的使用就会更加透彻一些,以Integer包装器为例:

 
// 源码中的静态内部类
 private static class IntegerCache {
        static final int low = -128;
        static final int high;
        static final Integer cache[];

        static {
            // high value may be configured by property
            int h = 127;
            String integerCacheHighPropValue =
                sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high");
            if (integerCacheHighPropValue != null) {
                try {
                    int i = parseInt(integerCacheHighPropValue);
                    i = Math.max(i, 127);
                    // Maximum array size is Integer.MAX_VALUE
                    h = Math.min(i, Integer.MAX_VALUE - (-low) -1);
                } catch( NumberFormatException nfe) {
                    // If the property cannot be parsed into an int, ignore it.
                }
            }
            high = h;

            cache = new Integer[(high - low) + 1];
            int j = low;
            for(int k = 0; k < cache.length; k++)
                cache[k] = new Integer(j++);

            // range [-128, 127] must be interned (JLS7 5.1.7)
            assert IntegerCache.high >= 127;
        }

        private IntegerCache() {}
    }


// 源码中的装箱操作
   public static Integer valueOf(int i) {
        if (i >= IntegerCache.low && i <= IntegerCache.high)
            return IntegerCache.cache[i + (-IntegerCache.low)];
        return new Integer(i);
   }

  代码中,只是通过静态内部类定义了缓存值的默认范围,在你的程序中,如果你没有主动的执行装箱或自动的执行装箱转换,静态内部类是不会加载进内存的,对内存没有任何占用;只有当你的代码有了装箱操作,才会对静态内部类产生调用,才会将该类加载进内存。从这个方面去想,Java的设计者通过对于代码的编写的优化是比我等菜鸟要高深那么一点点点点。。。。

   匿名内部类

  继续顾名思义,匿名内部类,就是没有类名的内部类。缺少了类名,也就意味着没有构造器,那怎么创建对象呢?只能直接new 它的类实体,可以这么说,匿名内部类是伴随着类定义的同时就必须被实例化的。我们通过匿名内部类来创建线程就是很好的例子:

public class ThreadTest {
    public static void main(String[] args) {
        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {

            }
        });
        thread.start();
    }
}

  (PS : 对Java8的Lambda表达式比较熟悉的老司机可能会像下面这样去写,看起来代码简洁逼格也挺高,不过说实话,这样写代码我看了心里是有句mmp不知当讲不当讲的,项目组的维护和沟通成本都很高。只能说,对于新技术还是不要盲目的跟风吧。)

public class ThreadTest {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
        });
        thread.start();
    }
}

  线程的创建应该是我们比较常见的匿名内部类的应用了。不过上诉代码你如果不用匿名内部类,也可以先实现了接口,通过接口的实现再来创建对象,但是,何必舍近道而绕远路呢?这里也可以看出匿名内部类特点了,它帮我们省去了单独实现接口再来创建对象的很多冗余的步骤。所以你该知道,匿名内部类本质上就是一种代码上的减省,实际上它还是在遵循着Java实现(继承)后再创建对象的语法逻辑的,不信看下面的代码:

public class Model {
    private int i;

    public Model(int i) {
        this.i = i;
    }

    public int value() {
        return i;
    }

    @Override
    public String toString() {
        return "Model{" +
                "i=" + i +
                '}';
    }
}
public class Demo {
    public Model getModel(int var){
        return new Model(var){
            public int value(){
                System.out.println("方法调用");
                return super.value()*20;
            }
        };
    }

    @Test
    public void test(){
        Model model = getModel(3);
        System.out.println(model);       //此时value的值还是 3 ,因为匿名类中的方法还没被调用
        System.out.println(model.value()); // 此时value被赋值为 60
    }
}

  这段段代码说明两点:① 通过匿名内部类的方式返回Model对象的时候是有继承体系的,也即匿名实体是继承了Model类的,不然实体中的无法使用super调用父类方法;② 不只是接口的实现可以用匿名内部类,普通类的继承也是可以的,只要满足继承体系即可。

4 、个人叨叨

内部类本人平时开发中用得也不多,单纯的从功能实现来讲,漫长的码农生涯中完全不用内部类也完全无碍,但是Java的设计者们为什么还要搞这套语法呢?追踪Java标准类库的一些源码你会发现,平时常用的容器类、整形包装器等都有大量使用内部类的场景;而平时引入的第三方类库、框架中的源码也有很多使用内部类的。没有对比就没有伤害,当你翻看自己的代码总是一坨一坨拼凑感极强,源码框架中的代码总是那么的优雅时,你还能说什么呢,还不能让你对代码编写质量,对内部类这种语法结构的使用多一份心思吗?尤其是在没有代码审查制度对代码质量也没有严格要求的公司,程序员的在代码上的任意发挥简直是程序bug之母,所以,像布衣博主一样,虽然对内部类在代码编写上的妙用,还体会得不够深刻,但优化代码时,尝试着往源码的思路靠,样子走,总还是有些益处的。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

扫码关注云+社区

领取腾讯云代金券