前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >10. Kotlin 类声明和构造器(constructor)

10. Kotlin 类声明和构造器(constructor)

作者头像
sickworm
修改2020-12-07 20:29:07
1.1K0
修改2020-12-07 20:29:07
举报
文章被收录于专栏:sickwormsickworm

1. Java 和 Kotlin 构造器代码对比

Java 的构造器声明和方法声明没有太大区别,也支持重载,唯一的限制是:必须调用父类构造器(如果父类只有一个构造器而且是无参的,编译器会帮你自动加上,这是特例)。我们使用 Java 多年,构造器很少会给我们带来不便,也不曾听人吐槽 Java 的构造器声明的不合理,便是无功无过,规规矩矩。但现代编程语言还是从构造器身上找到了优化空间,Scala–Kotlin 是其中之一。

我们不妨直接上代码对比 Kotlin 和 Java 的构造器声明的区别。现在我们有这样一个 android.view.View 的子类,它的 Java 实现:

代码语言:javascript
复制
public class RecordingBottomView extends ConstraintLayout implements View.OnClickListener {

    private static final String TAG = "RecordingBottomView";
    private static final int DEFAULT_VALUE = 100;
    private EffectBarDialog effectBarDialog = new EffectBarDialog(getContext());

    private TextView channelButton;
    public TextView effectButton;
    private TextView restartButton;
    private TextView finishBtn;

    private RecordPlayButton playButton;
    private TextView switchCameraButton;
    private TextView filterButton;
    private TextView resumeTips;

    private int defaultValue = DEFAULT_VALUE;

    public RecordingBottomView(Context context) {
        super(context);
        init(context, null, 0);
    }

    public RecordingBottomView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init(context, attrs, 0);
    }

    public RecordingBottomView(Context context, AttributeSet attrs, int defStyleAttr) {
        super(context, attrs, defStyleAttr);
        init(context, attrs, defStyleAttr);
    }

    private void init(Context context, AttributeSet attrs, int defStyleAttr) {
        LayoutInflater.from(context).inflate(R.layout.recording_bootom_panel, this, true);
        findViewById(R.id.recording_channel_switch_btn).setOnClickListener(this);
        channelButton = findViewById(R.id.recording_channel_switch_btn);
        effectButton = findViewById(R.id.recording_effect_btn);
        effectButton.setOnClickListener(this);


        restartButton = findViewById(R.id.recording_restart_btn);
        restartButton.setOnClickListener(this);
        finishBtn = findViewById(R.id.recording_finish_btn);
        finishBtn.setOnClickListener(this);

        switchCameraButton = findViewById(R.id.song_record_camera_switch);
        switchCameraButton.setOnClickListener(this);
        filterButton = findViewById(R.id.song_record_filter);
        filterButton.setOnClickListener(this);
        playButton = findViewById(R.id.recording_play_button);
        playButton.setOnClickListener(this);
        resumeTips = findViewById(R.id.song_record_resume_tip_btn);

        effectBarDialog.setSoundSelectListener(reverbId -> {
            LogUtil.i(TAG, "soundSelect");
            return Unit.INSTANCE;
        });
        effectBarDialog.setToneChangeListener((isUp, toneValue) -> {
            LogUtil.i(TAG, "toneChange");
            return Unit.INSTANCE;
        });
        effectBarDialog.setVolumeChangeListener(volume -> {
            LogUtil.i(TAG, "volumeChange");
            return Unit.INSTANCE;
        });

        if (attrs != null) {
            TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RecordingBottomView);
            defaultValue = ta.getInt(R.styleable.RecordingBottomView_default_value, DEFAULT_VALUE);
        }
    }

    @Override
    public void onClick(View v) {
        // handle click
    }
}

非常经典的代码,在构造器中初始化所有的子 View 成员变量以及 View 参数。

对应的,Kotlin 风格的实现如下:

代码语言:javascript
复制
class RecordingBottomView(context: Context, attrs: AttributeSet?, defStyleAttr: Int):
        ConstraintLayout(context, attrs, defStyleAttr),
        View.OnClickListener {

    constructor(context: Context): this(context, null, 0)

    constructor(context: Context, attrs: AttributeSet?): this(context, attrs, 0)

    init {
        LayoutInflater.from(context).inflate(R.layout.recording_bootom_panel, this, true)
        findViewById<View>(R.id.recording_channel_switch_btn).setOnClickListener(this)
    }

    private val effectBarDialog = EffectBarDialog(context).also {
        it.soundSelectListener = { reverbId: Int? ->
            LogUtil.i(TAG, "soundSelect")
        }
        it.toneChangeListener = { isUp: Boolean?, toneValue: Int? ->
            LogUtil.i(TAG, "toneChange")
        }
        it.volumeChangeListener = { volume: Float? ->
            LogUtil.i(TAG, "volumeChange")
        }
    }

    private val channelButton = findViewById<TextView>(R.id.recording_channel_switch_btn).also {
        it.setOnClickListener(this)
    }

    var effectButton = findViewById<TextView>(R.id.recording_effect_btn).also {
        it.setOnClickListener(this)
    }

    private val restartButton = findViewById<TextView>(R.id.recording_restart_btn).also {
        it.setOnClickListener(this)
    }

    private val finishBtn = findViewById<TextView>(R.id.recording_finish_btn).also {
        it.setOnClickListener(this)
    }

    private val playButton: RecordPlayButton? = null
    private val switchCameraButton = findViewById<TextView>(R.id.recording_finish_btn).also {
        it.setOnClickListener(this)
    }

    private val filterButton = findViewById<TextView>(R.id.recording_finish_btn).also {
        it.setOnClickListener(this)
    }

    private val resumeTips = findViewById<TextView>(R.id.song_record_resume_tip_btn)

    private val defaultValue: Int = let {
        attrs?: DEFAULT_VALUE

        val ta = context.obtainStyledAttributes(attrs, R.styleable.RecordingBottomView)
        val value = ta.getInt(R.styleable.RecordingBottomView_default_value, DEFAULT_VALUE)
        ta.recycle()
        value
    }

    override fun onClick(v: View) {}

    companion object {
        private const val TAG = "RecordingBottomView"
        private const val DEFAULT_VALUE = 100
    }
}

这里暂且不展开谈 默认参数,view binding,also let 闭包的知识点,这些知识点会在其他文章单独介绍。

对我而言,在我接触 Kotlin 这种构造器声明之前,我没有想过 Java 的构造器声明有什么缺点。但当我接触之后,我开始思考 Kotlin 为什么要这样设计构造器声明,以及 Java 构造器声明的不足之处:

代码语言:javascript
复制
1. **Java 构造器成员变量如果依赖构造参数,它们的声明和最终赋值是分离的,同一个成员变量的代码是低内聚的。**而且像```defaultValue```这个参数,虽然他有初始值,但在一定条件下,他可能会被重新赋值。这些问题都会增加阅读者的心智负担;

2. 所有的初始化代码都在一个函数中,很容易出现“超级函数”。**不同成员变量的初始化代码大部分互相没有联系,但是却以先后顺序的形式耦合在同一个函数中,这是高耦合的。**

3. Java 构造器允许重载,虽然设计规范提倡重载函数应最终调用同一个实现,以得到清晰的逻辑表达。但这不是强制性的。这意味着你可能会遇到多个构造器各自拥有自己的实现,这会加重问题 1,2 的严重性。

对应的,Kotlin 采用了如下对策来一一解决这些问题:

代码语言:javascript
复制
1. property 声明初始化时允许使用主构造器参数,变量声明和初始化代码都写在同一个地方,代码是高内聚的;

2. 使用 let 闭包后,成员变量的所有的初始化代码都可以写在闭包内。不同的成员变量初始化代码相互独立,代码是低耦合的;

3. 仅允许一个主构造器,其他构造器为从构造器,并约定从构造器必须调用主构造器,让主构造器去调用父构造器。

如果 Kotlin 类没有声明主构造器,全部都是从构造器,则退化为 Java 构造器风格,没有调用主构造器的约束。这样的设计一是为了 Java 转 Kotlin 代码时能兼容旧代码结构,不用重构也能直接转换为 Kotlin 代码;二也方便了 Java 转 Kotlin 自动化工具的实现。但 property 的初始化无法引用从构造器的入参,因为从构造器是可以有多个的,从调用上无法保证每个从构造器的每个参数都存在。

2. Kotlin 构造器实现分析

上面我们简单的过了一遍 Kotlin 对 Java 构造器的优化,但 Java 采用这样的设计,是因为它忠实的反映了 JVM 的构造器实现。而 Kotlin 的构造器设计,并不符合 JVM 的实现。Kotlin 要最终在 JVM 上运行,必须在编译期处理,最终变回类似 Java 构造器的实现。

我们直接看一下 Kotlin 编译再反编译后的字节码:

代码语言:javascript
复制
public RecordingBottomView(@NotNull Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        Intrinsics.checkParameterIsNotNull(context, "context");
        super(context, attrs, defStyleAttr);
        TypedArray ta = context.obtainStyledAttributes(attrs, R.styleable.RecordingBottomView);
        int value = ta.getInt(R.styleable.RecordingBottomView_default_value, 100);
        ta.recycle();
        this.defaultValue = value;
        LayoutInflater.from(context).inflate(R.layout.recording_bootom_panel, (ViewGroup) this, true);
        findViewById(R.id.recording_channel_switch_btn).setOnClickListener(this);
        EffectBarDialog it = new EffectBarDialog(context);
        it.setSoundSelectListener(RecordingBottomView$effectBarDialog$1$1.INSTANCE);
        it.setToneChangeListener(RecordingBottomView$effectBarDialog$1$2.INSTANCE);
        it.setVolumeChangeListener(RecordingBottomView$effectBarDialog$1$3.INSTANCE);
        this.effectBarDialog = it;
        View findViewById = findViewById(R.id.recording_channel_switch_btn);
        ((TextView) findViewById).setOnClickListener(this);
        this.channelButton = (TextView) findViewById;
        View findViewById2 = findViewById(R.id.recording_effect_btn);
        ((TextView) findViewById2).setOnClickListener(this);
        this.effectButton = (TextView) findViewById2;
        View findViewById3 = findViewById(R.id.recording_restart_btn);
        ((TextView) findViewById3).setOnClickListener(this);
        this.restartButton = (TextView) findViewById3;
        View findViewById4 = findViewById(R.id.recording_finish_btn);
        ((TextView) findViewById4).setOnClickListener(this);
        this.finishBtn = (TextView) findViewById4;
        View findViewById5 = findViewById(R.id.recording_finish_btn);
        ((TextView) findViewById5).setOnClickListener(this);
        this.switchCameraButton = (TextView) findViewById5;
        View findViewById6 = findViewById(R.id.recording_finish_btn);
        ((TextView) findViewById6).setOnClickListener(this);
        this.filterButton = (TextView) findViewById6;
        this.resumeTips = (TextView) findViewById(R.id.song_record_resume_tip_btn);
        RecordingBottomView recordingBottomView = this;
        if (attrs == null) {
            Integer.valueOf(100);
        }
    }

public RecordingBottomView(@NotNull Context context) {
    Intrinsics.checkParameterIsNotNull(context, "context");
    this(context, null, 0);
}

public RecordingBottomView(@NotNull Context context, @Nullable AttributeSet attrs) {
    Intrinsics.checkParameterIsNotNull(context, "context");
    this(context, attrs, 0);
}

可以看到,property 和 init 块的初始化代码会被顺序的放入主构造器中,也就是说代码是从上往下按顺序执行的。因此 Kotlin 的初始化代码不仅可以使用主构造器的参数,还可以使用比自己先初始化的 property 和 init 块。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2020年4月6日,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 1. Java 和 Kotlin 构造器代码对比
  • 2. Kotlin 构造器实现分析
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档