Java 基础夯实4:内部类的使用场景介绍

文章出自:安卓进阶学习指南主要贡献者:

  • Cloud9527

  • Alex_赵

  • Struggle

  • shixinzhang

读完本文你将了解:

  • 通过反编译介绍四种内部类

  • 结合实战介绍内部类的使用场景

背景介绍

Java 基础夯实4:内部类的使用场景介绍_第1张图片

大家好,这篇文章是 《安卓进阶技能树计划》 的第一部分 《Java 基础系列》 的第四篇,介绍内部类的实战使用场景。

我们做这个活动,除了要保证知识点的全面、完整,还想要让每一篇文章都有自己的思考,尽可能的将知识点与实践结合,努力让读者读了有所收获。每位小伙伴都有工作在身,每个知识点都需要经过思考、学习、写作、提交、审核、修改、编辑、发布等多个过程,所以整体下来时间就会慢一些,这里先向各位道歉。

《Java 基础系列》初步整理大概有 12 篇,主要内容为:

  1. 抽象类和接口 (完成)

  2. 内部类

  3. 修饰符

  4. 装箱拆箱

  5. 注解

  6. 反射

  7. 泛型

  8. 异常(完成)

  9. 集合

  10. IO

  11. 字符串

  12. 其他

内部类的使用场景

上一篇文章《Java 基础夯实3:通过字节码了解内部类》介绍了 Java 中 4 种内部类的定义,接着我们介绍这些内部类的一些使用场景。

1.成员内部类的使用场景

普通内部类可以访问外部类的所有成员和方法,因此当类 A 需要使用类 B ,同时 B 需要访问 A 的成员/方法时,可以将 B 作为 A 的成员内部类。

比如安卓开发中常见的在一个 Activity 中有一个 ListView,我们需要创建一个特定业务的 adapter,在这个 adapter 中需要传入数据,你可以另建一个类,但如果只有当前类需要使用到,完全可以将它创建在 Activity 中:

public class VideoListActivity extends AppCompatActivity{
    private ListView mVideoListView;
    private BaseAdapter mListAdapter;
    private List mVideoInfoData;

    @Override
    protected void onCreate(@Nullable final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_video_list);
        mVideoListView = (ListView) findViewById(R.id.video_list);
        mVideoInfoData = Collections.EMPTY_LIST;
        mListAdapter = new VideoListAdapter();
        mVideoListView.setAdapter(mListAdapter);
    }

    //这里的 private 内部类说明这个 adapter 只能在当前类中使用
    private class VideoListAdapter extends BaseAdapter {

        @Override
        public int getCount() {
            return mVideoInfoData.size();   //访问外部类数据
        }

        @Override
        public Object getItem(final int position) {
            return mVideoInfoData.get(position);    //访问外部类数据
        }

        @Override
        public long getItemId(final int position) {
            return 0;
        }

        @Override
        public View getView(final int position, final View convertView, final ViewGroup parent) {
            return null;
        }
    }
}

这是一种简单的使用场景。

在 Java 中普通类(非内部类)是不可以设为 private 或者 protected,只能设置成 public default

而内部类则可以,因此我们可以利用 private 内部类禁止其他类访问该内部类,从而做到将具体的实现细节完全隐藏。

比如我们有一个 Activity 既可以用作登录也可以用作注册,我们可以这样写:

public class MultiplexViewActivity extends AppCompatActivity {
    public static final String DATA_VIEW_TYPE = "view_type";
    public static final int TYPE_LOGIN = 1;
    public static final int TYPE_REGISTER = 2;

    private TextView mTitleTv;
    private ViewController mViewController;

    @Override
    protected void onCreate(@Nullable final Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_multiplex_view);

        int type = getIntent().getIntExtra(DATA_VIEW_TYPE, TYPE_LOGIN);
        mViewController = getViewController(type);

        initView();
    }

    //外界只能拿到基类,具体实现隐藏
   public ViewController getViewController(final int type) {
        switch (type) {
            case TYPE_REGISTER:
                return new RegisterViewController();
            case TYPE_LOGIN:
            default:
                return new LoginViewController();
        }
    }

    private void initView() {
        mTitleTv = (TextView) findViewById(R.id.multiplex_title_tv);
        mViewController.initUi();
    }

    /**
     * 定义操作规范
     */
    private interface ViewController {
        void initUi();

        void loadData();
    }

    private class LoginViewController implements ViewController {
        @Override
        public void initUi() {
            mTitleTv.setText("登录");
            //显示登录需要的布局
        }

        @Override
        public void loadData() {
            //加载登录需要的数据
        }
    }

    private class RegisterViewController implements ViewController {
        @Override
        public void initUi() {
            mTitleTv.setText("注册");
            //显示注册需要的布局
        }

        @Override
        public void loadData() {
            //加载注册需要的数据
        }
    }
}

解释一下上面的代码,由于要复用这个布局,所以先定义一个布局控制接口 ViewController,再创建两个内部类实现接口,分别负责登录和注册的布局控制和数据加载。

然后提供一个方法根据参数获取具体的控制器实现 getViewController(final int type),这个方法可以是 public的,外界即使拿到这个 activity 实例,也只能获取到布局控制器基类,具体的实现被隐藏了,这在后期修改某一个页面时,不用担心会对其他地方造成影响。

有朋友可能会说了:“这 2 个内部类也可以定义成普通类呀”。

确实普通类也同样能满足需求,但是我们希望这 2 个类只是在这个公共支付信息页面才用到,在外界看来是不可见或不可用的状态,这个时候内部类就能满足我们的需求。

这样的场景在 简单工厂模式、迭代器设计模式、命令设计模式都有用到,有兴趣的朋友可以去了解下。

2.静态内部类的使用场景

静态内部类只能访问外部类的静态变量和方法,但相对普通内部类的功能更为完整,因为它可以定义静态变量/方法

当类 A 需要使用类 B,而 B 不需要直接访问外部类 A 的成员变量和方法时,可以将 B 作为 A 的静态内部类。

比较常见的一种使用场景是:在基类 A 里持有静态内部类 B 的引用,然后在 A 的子类里创建特定业务的 B 的子类,这样就结合多态和静态内部类的优势,既能拓展,又能限制范围

我们经常使用的 LayoutParams 就是静态内部类,由于不同的布局中参数不一样,Android SDK 提供了很多种 LayoutParams:

  • ViewGroup.LayoutParams

  • WindowManager.LayoutParams 继承上一层

  • RelativeLayout.LayoutParams

  • ...

public interface WindowManager extends ViewManager {

    //...
    public static class LayoutParams extends ViewGroup.LayoutParams implements Parcelable {
    //...
    }
}

在 View 的 setLayoutParams 中的参数类型是最上层的 ViewGroup.LayoutParams params,这样子类就可以传入符合自己特性的 LayoutParams 实现:

public void setLayoutParams(ViewGroup.LayoutParams params) {
    if (params == null) {
        throw new NullPointerException("Layout parameters cannot be null");
    }
    mLayoutParams = params;
    resolveLayoutParams();
    if (mParent instanceof ViewGroup) {
        ((ViewGroup) mParent).onSetLayoutParams(this, params);
    }
    requestLayout();
}

静态内部类的另一种使用场景是:实现单例模式

记得有一年去点评面试,面试官让我写个静态内部类实现的单例模式,我写的过程中不确定静态内部类是否可以有静态成员,基础有多差可想而知。

先来看一下如何实现:

public class LocationManager{
    private static class ClassHolder {
        private static final LocationManager instance = new LocationManager();
    }
    public static LocationManager getInstance() {
        return ClassHolder.instance;
    }
}

我们知道静态内部类功能和普通类一致,所以有 static 成员不足为奇。现在的问题是,为什么这种单例模式比较好?

原因有两点:

  1. 懒加载:类加载时不会创建实例,只有当 getInstance() 方法被调用时才去加载静态内部类以及其中持有的 LocationManager 实例

  2. 线程安全:JVM 加载类时,可以确保 instance 变量只能初始化一次

3.匿名内部类的使用场景

Android 开发中设置一个按钮的点击事件很简单,直接 new 一个 View.OnClickListener 然后实现方法即可:

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

结合前面谈到的,编译器会为每个匿名内部类创建一个 Class 文件。个人觉得在安卓开发中,有多个按钮需要设置点击事件时,让当前类实现 OnClickListener 接口然后在 onClick() 中根据 id 判断事件,比创建一大堆匿名内部类要好些,你觉得呢?

之所以这样写,是因为我们不需要持有这个 new View.OnClickListener 的引用,只要创建了对象即可。

所以使用场景可以是:一个方法的返回值是接口,然后根据不同参数返回不同的实现,我们不需要保存引用,直接 new 一个接口实现即可。

来看一个有趣的例子:

public class GirlFriendMaker {
    public interface GirlFriend {
        void sayHi();
    }

    public static GirlFriend giveMeAGirlFriend(final String name) {
        return new GirlFriend() {    //匿名内部类
            @Override
            public void sayHi() {
                Log.i("来自女朋友的问候", "Hello I'm " + name);
            }
        };
    }
}

4.局部内部类

局部内部类只用于当前方法或者代码块中创建、使用,一次性产品,使用场景比较少。

内存泄漏

经过前面的介绍我们知道,四种内部类中除了静态内部类,只要访问外部类的成员/方法,就会持有外部类的引用。

当内部类持有外部类的引用,同时生命周期比外部类要长(比如执行耗时任务、被其他长生命周期对象持有),就会导致外部类该被回收时无法被回收,也就是内存泄漏问题。

一个 Android 开发中常见的内部类导致内存泄露的例子:

public class MainActivity extends AppCompatActivity {

    public final int LOGIN_SUCCESS = 1;

    private Context mContext;
    private boolean isLongTimeNoMsg;


    @SuppressWarnings("HandlerLeak")
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            isLongTimeNoMsg = false;
            switch (msg.what) {
                case LOGIN_SUCCESS: {/
                    break;
                }
                //...
    }
}

这个 Handler 持有外部类的引用,它发送的 runnable 对象,会被进一步包装为 message 对象,放入消息队列,在被执行、回收之前会一致持有引用,导致无法释放。

解决办法就是使用弱引用或者干脆将 Handler 设计为静态内部类。

总结

总的来说,内部类一般用于两个场景:

  1. 需要用一个类来解决一个复杂的问题,但是又不希望这个类是公共的

  2. 需要实现一个接口,但不需要持有它的引用

本篇文章介绍了 Java 开发中四种内部类的概念、反编译后的格式以及使用场景。相信看完这篇文章,你对开头的两道题已经有了答案。

基础就是这样,不论你走的多远,都需要及时回顾、弥补,等工作中需要用到才补,会错失很多机会。

这个系列的目的是帮助大家系统、完整的打好基础、逐渐深入学习,如果你对这些已经很熟了,请不要吝啬你的评价,多多指出问题,我们一起做的更好!

Java 基础夯实4:内部类的使用场景介绍_第2张图片

0?wx_fmt=png


你可能感兴趣的:(Java 基础夯实4:内部类的使用场景介绍)