在java应用开发中,我们经常会遇到多个类具有相似的属性和初始化步骤,但具体类型或某些细节有所不同的场景。例如,在一个android应用中,可能有多种ui元素(如 loadelement 和 errorelement),它们都需要初始化一个 viewdatabinding 对象,并设置其生命周期所有者(lifecycleowner)。尽管具体的 binding 类型不同(loadingelementbinding vs errorelementbinding),但初始化流程中的大部分逻辑是共享的。
考虑以下初始代码结构,其中 LoadElement 和 ErrorElement 各自维护并初始化自己的 binding:
public class LoadElement {
LoadingElementBinding binding;
public LoadElement(ViewGroup parent) {
binding = LoadingElementBinding.inflate(
LayoutInflater.from(parent.getContext()),
parent,
false);
binding.setLifecycleOwner(ViewTreeLifecycleOwner.get(parent));
}
public void doSomething() {
// 与 binding 相关的业务逻辑
}
}
public class ErrorElement {
ErrorElementBinding binding;
public ErrorElement(ViewGroup parent) {
binding = ErrorElementBinding.inflate(
LayoutInflater.from(parent.getContext()),
parent,
false);
binding.setLifecycleOwner(ViewTreeLifecycleOwner.get(parent));
}
public void doSomething() {
// 与 binding 相关的业务逻辑
}
}这种模式导致了代码重复,难以维护。理想情况下,我们希望能够将这些通用的初始化代码进行抽象和复用。
一种直观的解决方案是引入一个抽象基类 BindingElement,并在其中定义一个抽象方法来创建特定类型的 binding,然后在基类的构造器中调用这个抽象方法。
public abstract class BindingElement{ T binding; public BindingElement (ViewGroup parent) { // 尝试在构造器中调用抽象方法 binding = createBinding(LayoutInflater.from(parent.getContext()), parent); binding.setLifecycleOwner(ViewTreeLifecycleOwner.get(parent)); } // 抽象方法,由子类实现具体的 binding 创建逻辑 abstract T createBinding(LayoutInflater inflater, ViewGroup parent); public void doSomething() { // 共享的业务逻辑 } } public class LoadElement extends BindingElement { public LoadElement(ViewGroup parent) { super(parent); } @Override LoadingElementBinding createBinding(LayoutInflater inflater, ViewGroup parent){ return LoadingElementBinding.inflate(inflater, parent, false); } } // ... 其他子类类似
然而,这种做法在Java中存在潜在的风险。在Java中,当一个对象的构造器被调用时,其父类的构造器会首先执行。如果在父类构造器中调用了一个非 final 或 private 的方法(尤其是抽象方法),并且该方法在子类中被重写,那么在子类完全初始化之前,子类重写的方法可能会被调用。这可能导致访问到尚未初始化完成的子类成员变量,从而引发 NullPointerException 或其他不可预测的行为。这种设计模式通常被认为是“构造器中调用虚方法”的反模式。
为了安全且优雅地解决上述问题,我们可以利用Java 8引入的函数式接口(FunctionalInterface)和方法引用(Method Reference)。核心思想是将具体的 binding 创建逻辑封装在一个函数式接口中,并通过构造器参数将其传递给抽象基类。这样,基类在构造时接收的是一个已准备好的“创建器”,而不是主动调用一个尚未完全确定的抽象方法。
首先,我们定义一个函数式接口 BindingCreator,它包含一个抽象方法,用于创建特定类型的 ViewDataBinding 实例。
@FunctionalInterface public interface BindingCreator{ /** * 创建一个 ViewDataBinding 实例。 * @param inflator 用于布局膨胀的 LayoutInflater * @param parent 父视图组 * @param attachToParent 是否将布局附加到父视图组 * @return 创建的 ViewDataBinding 实例 */ T createBinding(LayoutInflater inflator, ViewGroup parent, boolean attachToParent); }
这个接口的签名与 ViewDataBinding 的静态 inflate 方法兼容。
修改 BindingElement 抽象类的构造器,使其接受一个 BindingCreator 实例作为参数。在构造器内部,通过这个 BindingCreator 实例来执行 binding 的创建逻辑。
import android.view.LayoutInflater; import android.view.ViewGroup; import androidx.lifecycle.ViewTreeLifecycleOwner; import androidx.databinding.ViewDataBinding; public abstract class BindingElement{ protected T binding; // 建议设为 protected 以便子类访问 /** * 构造函数,通过 BindingCreator 接收具体的 binding 创建逻辑。 * @param parent 父视图组 * @param bindingCreator 用于创建 ViewDataBinding 实例的函数式接口 */ public BindingElement(ViewGroup parent, BindingCreator bindingCreator){ // 使用传入的 bindingCreator 来创建 binding 实例 binding = bindingCreator.createBinding( LayoutInflater.from(parent.getContext()), parent, false); // 假设 false 是一个通用参数,或可由子类传递 binding.setLifecycleOwner(ViewTreeLifecycleOwner.get(parent)); } public void doSomething() { // 共享的业务逻辑,可操作 binding } // 允许子类或外部访问 binding public T getBinding() { return binding; } }
现在,具体的子类 LoadElement 和 ErrorElement 只需要在其构造器中调用 super(),并传入对应 Binding 类的静态 inflate 方法的方法引用即可。
import android.view.ViewGroup; import com.example.app.LoadingElementBinding; // 假设这是你的 binding 类 import com.example.app.ErrorElementBinding; // 假设这是你的 binding 类 public class LoadElement extends BindingElement{ public LoadElement(ViewGroup parent) { // 通过方法引用将 LoadingElementBinding::inflate 作为 BindingCreator 传入 super(parent, LoadingElementBinding::inflate); } // ... 可以有 LoadElement 特有的方法 } public class ErrorElement extends BindingElement { public ErrorElement(ViewGroup parent) { // 通过方法引用将 ErrorElementBinding::inflate 作为 BindingCreator 传入 super(parent, ErrorElementBinding::inflate); } // ... 可以有 ErrorElement 特有的方法 }
始化逻辑,如果仅仅通过一个方法引用不足以表达,可能需要调整 BindingCreator 的方法签名,或者考虑引入更复杂的工厂模式(但通常函数式接口足以应对大多数情况)。通过巧妙地结合Java 8的函数式接口和方法引用,我们能够以一种安全、优雅且高效的方式来组织和重用具有相似初始化逻辑的代码。这种模式不仅解决了传统面向对象设计中“构造器中调用虚方法”的陷阱,还提升了代码的模块化、可读性和可维护性。在处理类似的初始化场景时,优先考虑使用这种函数式编程与面向对象设计相结合的模式,将有助于构建更加健壮和灵活的Java应用程序。