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

2020-12-07 20:29:07 浏览数 (1)

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 块。

0 人点赞