章节索引 :

Kotlin 数据类、密封类、内部类和嵌套类

从这篇文章我们一起来看下 Kotlin 几个比较特殊的类,其中数据类 (data class) 和密封类 (sealed class) 是 Java 中不存在的,所以下面会一一介绍它们如何使用、使用场景、它们解决了哪些问题以及语法糖背后的原理。此外在 Kotlin 中存在嵌套类和内部类,需要注意的是 Kotlin 的嵌套类不是内部类,这点和 Java 是不一样的,所以内部嵌套类不能访问外部类实例。在 Kotlin 中内部类声明需要单独使用 inner 关键字声明。

1. 内部类和嵌套类

我们都知道在 Java 中表示内部类实际上就是将内部类嵌套在外部类中就声明了一个内部类,那么内部类就能访问外部类私有成员

//PageAdapter
public abstract class PageAdapter {
    public abstract int getCount();

    public abstract String getItem(int position);
}

//PageTest
package com.imooc.test;

import java.util.Arrays;
import java.util.List;

public class PageTest {

    private List<String> mData = Arrays.asList("1", "2", "3");

    class TestPageAdapter extends PageAdapter {//在Java中只需要把内部类TestPageAdapter声明在外部类PageTest内部即可
        @Override
        public int getCount() {
            return mData.size();//内部类即可以访问外部私有成员
        }

        @Override
        public String getItem(int position) {
            return mData.get(position);
        }
    }
}

然而在 Kotlin 中不是这样的,对于 Kotlin 中在一个类内部再声明一个类我们把它称为嵌套类,嵌套类是不能直接访问外部类的私有成员的。

package com.imooc.test

class PageTestKt {
    private val mData = listOf<String>("1", "2", "3")

    class TestPageAdapter : PageAdapter() {
        override fun getItem(position: Int): String {
           return mData[position]//由于无法访问mData,所以mData[position]编译报错
        }

        override fun getCount(): Int {
            return mData.size//由于无法访问mData,所以mData.size编译报错
        }

    }

为什么 Kotlin 嵌套类不能直接访问外部类私有成员,我们可以把它反编译成 Java 代码就一目了然了:

public final class PageTestKt {
   private final List mData = CollectionsKt.listOf(new String[]{"1", "2", "3"});

   public static final class TestPageAdapter extends PageAdapter {//可以看到实际上Kotlin嵌套类就是一个static静态类,所以它肯定不能访问外部类PageTestKt私有成员mData
      @NotNull
      public String getItem(int position) {
         return "";
      }

      public int getCount() {
         return 0;
      }
   }
}

可以看到实际上 Kotlin 嵌套类就是一个 static 静态类,所以它肯定不能访问外部类 PageTestKt 私有成员 mData。如果要在 Kotlin 声明一个内部类,应该怎么做呢?很简单只需要在嵌套类基础上加上一个 inner 关键字声明即可。

package com.imooc.test

class PageTestKt {
    private val mData = listOf<String>("1", "2", "3")

    inner class TestPageAdapter : PageAdapter() {//inner关键字声明一个Kotlin中的内部类
        override fun getItem(position: Int): String {
            return mData[position]//由于TestPageAdapter是PageTestKt的内部类,那么它就可以直接访问外部类私有属性mData
        }

        override fun getCount(): Int {
            return mData.size
        }

    }
}

为了进一步验证 inner class 实际上就是对应 Java 中的内部类,我们可以上述代码反编译成 Java 代码验证下:

public final class PageTestKt {
   private final List mData = CollectionsKt.listOf(new String[]{"1", "2", "3"});

   public final class TestPageAdapter extends PageAdapter {//可以看到TestPageAdapter确实是PageTestKt内部类,所以能直接访问外部类的私有成员mData
      @NotNull
      public String getItem(int position) {
         return (String)PageTestKt.this.mData.get(position);
      }

      public int getCount() {
         return PageTestKt.this.mData.size();
      }
   }
}

总结一下:声明嵌套类时,在 Java 中是在外部类内部使用 static class A , 而在 Kotlin 中只需要在外部类内部使用 class A 即可;声明内部时,在 Java 中只需要在外部类内部使用 class A , 而在 Kotlin 中则需要在外部类内部使用 inner class A

类 A 在类 B 内部声明 在 Java 中 在 Kotlin 中
嵌套类 (不能直接访问外部类私有属性) static class A class A
内部类 (能直接访问外部类私有属性) class A inner class A

2. 密封类 (sealed class)

2.1 为什么需要密封类

在开发中我们可能经常遇到这样一个场景,在使用 when 表达式的时候,最终需要返回表达式的一个值。由于 when 必须要有一个 else 分支情况,因为 Kotlin 编译器会强制检查默认选项。所以我们一般会返回一个无效值,可是有时候我们又不能返回一个合适的值,所以我们一般还会抛出一个异常来处理。一起来看个例子:

interface Expression

class Num(val value: Int): Expression

class Sum(val left: Expression, val right: Expression): Expression

fun eval(e: Expression): Int = when(e) {
    is Num -> e.value
    is Sum -> eval(e.right) + eval(e.left)
    else -> throw IllegalArgumentException("Unknown Expression")//由于Kotlin编译器必须要有else分支,这种不存在分支我们只能抛出异常
}

其实上面例子写法处理起来不是很优雅,此时我们就可以借助密封类来替代 Expression 接口,这样我们就可以省去这个 else 不必要分支了,编译器也不会报错。

sealed class Expression {//使用sealed关键字声明一个密封类
    class Num(val value: Int) : Expression()//然后把需要的类声明在密封类的内部

    class Sum(val left: Expression, val right: Expression) : Expression()
}

fun eval(e: Expression): Int = when (e) {
    is Expression.Num -> e.value
    is Expression.Sum -> eval(e.right) + eval(e.left)//在when判断的时候就可以省去不必要else判断
}

2.2 如何使用密封类

密封类使用 sealed 关键字声明,只需要把相应类声明在密封类内部即可,然后内部类继承基类。

sealed class Expression {//使用sealed关键字声明一个密封类
    class Num(val value: Int) : Expression()//把需要的类声明在密封类的内部,然后内部类继承基类

    class Sum(val left: Expression, val right: Expression) : Expression()
}

2.3 密封类的原理

密封类原理其实挺简单,实际上就是在一个私有的抽象类内部再声明多个 Java 中嵌套类也就是 static class . 可以把上述代码反编译成 Java 代码验证下:

public abstract class Expression {
   private Expression() {//密封类构造器私有化,防止密封类被外部实例化
   }

   // $FF: synthetic method
   public Expression(DefaultConstructorMarker $constructor_marker) {
      this();
   }

   public static final class Num extends Expression {//声明成static class静态类,也就是Expression的嵌套类
      private final int value;

      public final int getValue() {
         return this.value;
      }

      public Num(int value) {
         super((DefaultConstructorMarker)null);
         this.value = value;
      }
   }

   public static final class Sum extends Expression {//声明成static class静态类,也就是Expression的嵌套类
      @NotNull
      private final Expression left;
      @NotNull
      private final Expression right;

      @NotNull
      public final Expression getLeft() {
         return this.left;
      }

      @NotNull
      public final Expression getRight() {
         return this.right;
      }

      public Sum(@NotNull Expression left, @NotNull Expression right) {
         Intrinsics.checkParameterIsNotNull(left, "left");
         Intrinsics.checkParameterIsNotNull(right, "right");
         super((DefaultConstructorMarker)null);
         this.left = left;
         this.right = right;
      }
   }
}

3. 数据类 (data class)

3.1 为什么需要数据类

Java 开发小伙伴相信我们对 JavaBean 都非常的熟悉,当我们要定义一个数据模型时,就需要为其中每个属性定义 getter,setter 方法。如果要进行对象值的比较,甚至还需要重写 hashcode,equals 等方法。而对于 Kotlin 有时候并不需要这么啰嗦语法模板,它会让你实现起来更加简单高效。

import java.util.Objects;

public class Student {
    private String name;
    private int age;
    private double weight;
    private String nickName;
    private String address;

    public Student(String name, int age, double weight, String nickName, String address) {
        this.name = name;
        this.age = age;
        this.weight = weight;
        this.nickName = nickName;
        this.address = address;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public double getWeight() {
        return weight;
    }

    public void setWeight(double weight) {
        this.weight = weight;
    }

    public String getNickName() {
        return nickName;
    }

    public void setNickName(String nickName) {
        this.nickName = nickName;
    }

    public String getAddress() {
        return address;
    }

    public void setAddress(String address) {
        this.address = address;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof Student)) return false;
        Student student = (Student) o;
        return age == student.age &&
                Double.compare(student.weight, weight) == 0 &&
                name.equals(student.name) &&
                nickName.equals(student.nickName) &&
                address.equals(student.address);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age, weight, nickName, address);
    }

    @Override
    public String toString() {
        return "Student{" +
                "name='" + name + '\'' +
                ", age=" + age +
                ", weight=" + weight +
                ", nickName='" + nickName + '\'' +
                ", address='" + address + '\'' +
                '}';
    }
}

Kotlin 使用数据类实现 Student POJO 类

//你没看错,子啊Java中需要86行代码,在Kotlin中实现仅仅需要一行代码即可,实际上data class数据类编译器背后做了很多处理
data class Student(var name: String, var age: Int, var weight: Double, var nickName: String, var address: String)

3.2 如何使用数据类

对于 Kotlin 中数据类使用非常简单,只要在普通类声明的基础前面加上 data 关键字即可。数据类一般用于 Kotlin 描述数据模型。

data class Student(var name: String, var age: Int, var weight: Double, var nickName: String, var address: String)

3.3 数据类的原理

我们通过对比发现 Kotlin 中仅需要一行代码就能实现 Java 中 86 行代码功能,数据类到底有什么奥秘能做到这一点,其实这些都是编译器帮我们自动生成好的,我们可以通过反编译数据类 Kotlin 代码看下:

public final class Student {
   @NotNull
   private String name;
   private int age;
   private double weight;
   @NotNull
   private String nickName;
   @NotNull
   private String address;

    //为属性生成了setter和getter方法
   @NotNull
   public final String getName() {
      return this.name;
   }

   public final void setName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.name = var1;
   }

   public final int getAge() {
      return this.age;
   }

   public final void setAge(int var1) {
      this.age = var1;
   }

   public final double getWeight() {
      return this.weight;
   }

   public final void setWeight(double var1) {
      this.weight = var1;
   }

   @NotNull
   public final String getNickName() {
      return this.nickName;
   }

   public final void setNickName(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.nickName = var1;
   }

   @NotNull
   public final String getAddress() {
      return this.address;
   }

   public final void setAddress(@NotNull String var1) {
      Intrinsics.checkParameterIsNotNull(var1, "<set-?>");
      this.address = var1;
   }

    //生成了构造器方法
   public Student(@NotNull String name, int age, double weight, @NotNull String nickName, @NotNull String address) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      Intrinsics.checkParameterIsNotNull(nickName, "nickName");
      Intrinsics.checkParameterIsNotNull(address, "address");
      super();
      this.name = name;
      this.age = age;
      this.weight = weight;
      this.nickName = nickName;
      this.address = address;
   }

    //生成了component 1...N的解构函数,这个下面会详细说明
   @NotNull
   public final String component1() {
      return this.name;
   }

   public final int component2() {
      return this.age;
   }

   public final double component3() {
      return this.weight;
   }

   @NotNull
   public final String component4() {
      return this.nickName;
   }

   @NotNull
   public final String component5() {
      return this.address;
   }

    //生成了copy函数,这个下面会详细说明
   @NotNull
   public final Student copy(@NotNull String name, int age, double weight, @NotNull String nickName, @NotNull String address) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      Intrinsics.checkParameterIsNotNull(nickName, "nickName");
      Intrinsics.checkParameterIsNotNull(address, "address");
      return new Student(name, age, weight, nickName, address);
   }

   // $FF: synthetic method
   public static Student copy$default(Student var0, String var1, int var2, double var3, String var5, String var6, int var7, Object var8) {
      if ((var7 & 1) != 0) {
         var1 = var0.name;
      }

      if ((var7 & 2) != 0) {
         var2 = var0.age;
      }

      if ((var7 & 4) != 0) {
         var3 = var0.weight;
      }

      if ((var7 & 8) != 0) {
         var5 = var0.nickName;
      }

      if ((var7 & 16) != 0) {
         var6 = var0.address;
      }

      return var0.copy(var1, var2, var3, var5, var6);
   }

    //自动生成了toString
   @NotNull
   public String toString() {
      return "Student(name=" + this.name + ", age=" + this.age + ", weight=" + this.weight + ", nickName=" + this.nickName + ", address=" + this.address + ")";
   }

    //自动生成了hashCode方法
   public int hashCode() {
      String var10000 = this.name;
      int var1 = ((var10000 != null ? var10000.hashCode() : 0) * 31 + this.age) * 31;
      long var10001 = Double.doubleToLongBits(this.weight);
      var1 = (var1 + (int)(var10001 ^ var10001 >>> 32)) * 31;
      String var2 = this.nickName;
      var1 = (var1 + (var2 != null ? var2.hashCode() : 0)) * 31;
      var2 = this.address;
      return var1 + (var2 != null ? var2.hashCode() : 0);
   }

     //自动生成了equals方法
   public boolean equals(@Nullable Object var1) {
      if (this != var1) {
         if (var1 instanceof Student) {
            Student var2 = (Student)var1;
            if (Intrinsics.areEqual(this.name, var2.name) && this.age == var2.age && Double.compare(this.weight, var2.weight) == 0 && Intrinsics.areEqual(this.nickName, var2.nickName) && Intrinsics.areEqual(this.address, var2.address)) {
               return true;
            }
         }

         return false;
      } else {
         return true;
      }
   }
}

3.4 copy 方法

我们现在重点来分析下 copy 方法

   @NotNull
   public final Student copy(@NotNull String name, int age, double weight, @NotNull String nickName, @NotNull String address) {
      Intrinsics.checkParameterIsNotNull(name, "name");
      Intrinsics.checkParameterIsNotNull(nickName, "nickName");
      Intrinsics.checkParameterIsNotNull(address, "address");
      return new Student(name, age, weight, nickName, address);
   }

   // $FF: synthetic method
   public static Student copy$default(Student var0, String var1, int var2, double var3, String var5, String var6, int var7, Object var8) {//var0表示被copy的对象
      if ((var7 & 1) != 0) {
         var1 = var0.name;//copy时如果未指定具体属性的值,则会使用被copy对象的属性值
      }

      if ((var7 & 2) != 0) {
         var2 = var0.age;
      }

      if ((var7 & 4) != 0) {
         var3 = var0.weight;
      }

      if ((var7 & 8) != 0) {
         var5 = var0.nickName;
      }

      if ((var7 & 16) != 0) {
         var6 = var0.address;
      }

      return var0.copy(var1, var2, var3, var5, var6);
   }

需要注意的是 copy 方法主要是帮助我们从已有的数据类对象中拷贝一个新的数据类对象。当然也可以传入不同参数来生成不同的对象
,但是从代码中可以看出,如果一个属性并没有指定具体属性值,那么新生成的对象的值将使用被 copy 对象的属性值,也就是我们俗称的浅拷贝。

3.5 componentN 与对象解构声明

对象解构声明定义

解构声明是把一个对象看成一组单独的变量,有时候我们把一个对象看成一组单独的变量管理会变得更加简单。注意:支持解构声明的对象的类必须是数据类 (使用 data 关键字修饰的类), 因为只有 data class 才会生成对应的 component () 方法 (这个会在后续中讲解到),data class 中的每个属性都会有对应的 component () 方法对应。

对象解构的使用

//1.定义一个data class
data class Student(var name: String, var age: Int, var weight: Double, var nickName: String, var address: String)

//2. 解构声明调用
fun main() {
    val student = Student("imooc", 18, 88.0, "imooc", "北京市")
    val (name, age, weight, nickName, address) = student//将一个student对象解构成一组5个单独的变量
    println("my name is $name , I'm $age years old, weight is $weight, nickname is $nickName, address is $address")//解构后的5个变量可以脱离对象,直接单独使用
}

对象解构的原理

解构声明实际上就是将对象中所有属性,解构成一组属性变量,而且这些变量可以单独使用,为什么可以单独使用,是因为每个属性值的获得最后都编译成通过调用与之对应的 component () 方法,每个 component () 方法对应着类中每个属性的值,然后在作用域定义各自属性局部变量,这些局部变量存储着各自对应属性的值,所以看起来变量可以单独使用,实际上使用的是局部变量。如下反编译成的 Java 代码

public final class StudentKt {
   public static final void main() {
      Student student = new Student("imooc", 18, 88.0D, "imooc", "北京市");
      String name = student.component1();//对应的component1()方法,返回对应就是Student中name属性,并赋值给创建局部变量name
      int age = student.component2();//对应的component2()方法,返回对应就是Student中age属性,并赋值给创建局部变量age
      double weight = student.component3();//对应的component3()方法,返回对应就是Student中weight属性,并赋值给创建局部变量weight
      String nickName = student.component4();//对应的component4()方法,返回对应就是Student中nickName属性,并赋值给创建局部变量nickName
      String address = student.component5();//对应的component5()方法,返回对应就是Student中address属性,并赋值给创建局部变量address
      String var7 = "my name is " + name + " , I'm " + age + " years old, weight is " + weight + ", nickname is " + nickName + ", address is " + address;
      boolean var8 = false;
      System.out.println(var7);
   }

   // $FF: synthetic method
   public static void main(String[] var0) {
      main();
   }
}

4. 总结

到这里有关 Kotlin 面向对象中几种特殊类算是一一阐述完毕了,其中数据类和内部类用得比较多一点,对于密封类的使用在平时开发中用的不多,不过通过这篇文章你应该知道什么时候使用密封类,希望遇到相应的场景能够把它很好地用起来,这样代码绝对能体现 Kotlin 基础非常扎实。