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