概要

KotlinSwingコンポーネントを使用するサンプルと、Javaからの変換に関するメモです。

実行環境

$ curl -s "https://get.sdkman.io" | bash
$ sdk install kotlin
$ kotlinc -version
info: kotlinc-jvm 1.3.0 (JRE 1.8.0_192-b12)
$ kotlinc hello.kt -include-runtime -d hello.jar && "$JAVA_HOME/bin/java" -jar hello.jar

IntelliJの自動変換

  • メニューのCode, Convert Java File to Kotlin File(Ctrl+Alt+Shift+K)で、JavaのソースコードをKotlinのソースコードに変換可能
    • Hoge.javaを変換するとHoge.ktが生成されて、Hoge.javaは削除されてしまうので注意が必要
  • Fileメニュー、Project StructureパネルのProject SDK<No SDK>になっていると、@Overrideoverrideに変換されない場合がある

演算子の優先順位

  • 以下のようなソースコードをIntelliJで自動変換した場合、論理演算子の左シフト(<<)はshlに変換され、またandの優先順位が異なるので括弧が減る

Java

btn.setSelected((i & (1 << 2)) != 0);

Kotlin(自動変換)

btn.setSelected(i and (1 shl 2) != 0)
// 時々?に誤変換される場合がある?
// btn.setSelected(i and (1 shl 2 != 0))

シフト論理演算子

  • 論理演算子をIntelliJで自動変換した場合、括弧の対応がおかしくなる場合がある?

Java

@Override public int filterRGB(int x, int y, int argb) {
  int r = (int) Math.min(0xFF, ((argb >> 16) & 0xFF) * SCALE);
  int g = (int) Math.min(0xFF, ((argb >> 8) & 0xFF) * SCALE);
  int b = (int) Math.min(0xFF, (argb & 0xFF) * SCALE);
  return (argb & 0xFF000000) | (r << 16) | (g << 8) | b;
}

Kotlin(自動変換)

// - 0xFF が 0xFFf と float に変換される?
// - Math.min の括弧がおかしくなっている?
override fun filterRGB(x: Int, y: Int, argb: Int): Int {
  val r = Math.min(0xFFf, (argb shr 16 and 0xFF) * SCALE).toInt()
  val g = Math.min(0xFFf, (argb shr 8 and 0xFF) * SCALE).toInt()
  val b = Math.min(0xFFf, (argb and 0xFF) * SCALE).toInt()
  return argb and -0x1000000 or (r shl 16) or (g shl 8) or b
}

Kotlin(手動修正)

override fun filterRGB(x: Int, y: Int, argb: Int): Int {
  val r = Math.min(0xFF, ((argb shr 16 and 0xFF) * SCALE).toInt())
  val g = Math.min(0xFF, ((argb shr 8 and 0xFF) * SCALE).toInt())
  val b = Math.min(0xFF, ((argb and 0xFF) * SCALE).toInt())
  return argb and -0x1000000 or (r shl 16) or (g shl 8) or b
}

型パラメータ

  • IntelliJの自動変換ではArrays.asList(...)の型パラメータを自動的に変換してくれない
    • Arrays.<Component>asList(...)と型パラメータを明示しても省略される
    • for (c in Arrays.asList(...)) {に変換されてerror: none of the following functions can be called with the arguments supplied:になる
    • for (c: Component in Arrays.asList<Component>(...)) {、またはfor (c in listOf<Component>(...)) {などに手動で修正(for (c: Component in Arrays<Component>.asList(...)) {も可能?)
  • 型引数
    • JavaKotlinでは型引数を付ける位置が異なるので注意
  • KotlinJava風にfor (c in Arrays.<Component>asList(...)) {を使用すると、error: expecting an elementとエラーになる

Java

Box box = Box.createVerticalBox();
for (Component c: Arrays.<Component>asList(textField1, textField2, combo3, combo4)) {
  box.add(c);
  box.add(Box.createVerticalStrut(5));
}
// for (Component c: Arrays.asList(...)) { でも同じ変換結果になる

Kotlin(自動変換)

val box = Box.createVerticalBox()
for (c in Arrays.asList(textField1, textField2, combo3, combo4)) {
  box.add(c)
  box.add(Box.createVerticalStrut(5))
}
// error: none of the following functions can be called with the arguments supplied:
// public open fun add(p0: Component!): Component! defined in javax.swing.Box
// public open fun add(p0: PopupMenu!): Unit defined in javax.swing.Box
//             box.add(c)
//                 ^

Kotlin(手動修正)

val box = Box.createVerticalBox()
for (c in Arrays.asList<Component>(textField1, textField2, combo3, combo4)) {
  box.add(c);
  box.add(Box.createVerticalStrut(5));
}
// or:
// for (c in listOf<Component>(textField1, textField2, combo3, combo4)) {
// for (c in Arrays<Component>.asList(textField1, textField2, combo3, combo4)) {

境界型パラメータ

  • IntelliJの自動変換で以下のような境界型パラメータをもつJavaクラスを変換し、kotlincでコンパイルすると無駄な境界型パラメータが存在すると警告してくれる
    • warning: 'LocalDate' is a final type, and thus a value of the type parameter is predetermined
    • LocalDateクラスはfinalで継承不可のため、境界型パラメータは無意味
    • ECJ(Eclipse Compiler for Java)もThe type parameter T should not be bounded by the final type LocalDate. Final types cannot be further extendedと警告してくれる
    • Java側をclass CalendarViewTableModel<T extends TemporalAdjuster> extends DefaultTableModel {か、class CalendarViewTableModel extends DefaultTableModel {に修正する

Java

class CalendarViewTableModel<T extends LocalDate> extends DefaultTableModel {
  public CalendarViewTableModel(T date) {

Kotlin(自動変換)

class CalendarViewTableModel<T : LocalDate> public constructor(date: T) : DefaultTableModel() {

クラスリテラル

  • 総称型を持つクラスのクラスリテラル(Class Literals)をIntelliJで自動変換すると失敗する場合がある
    • 例えばJComboBox.classIntelliJの自動変換でJComboBox<*>::class.javaに変換されるが、JComboBox::class.javaに修正しないとコンパイルエラーになる
    • Only classes are allowed on the left hand side of a class literal
  • 逆にJavac instanceof JComboBoxKotlinに変換するときは、c is JComboBox<*>にしないとエラーになる
    • こちらはIntelliJの自動変換で正常に変換可能
    • One type argument expected. Use 'JComboBox<*>' if you don't want to pass type arguments

Java

enabledCheck.addActionListener(e -> {
  Container c = SwingUtilities.getAncestorOfClass(JComboBox.class, this);
  if (c instanceof JComboBox) {
    JComboBox<?> combo = (JComboBox<?>) c;
    ComboItem item = (ComboItem) combo.getItemAt(data.getIndex());
    item.setEnabled(((JCheckBox) e.getSource()).isSelected());
    editableCheck.setEnabled(item.isEnabled());
    textField.setEnabled(item.isEnabled());
  }
});

Kotlin(自動変換)

enabledCheck.addActionListener({ e ->
  val c = SwingUtilities.getAncestorOfClass(JComboBox<*>::class.java, this)
  if (c is JComboBox<*>) {
    val combo = c as JComboBox<*>
    val item = combo.getItemAt(data.index) as ComboItem
    item.isEnabled = (e.getSource() as JCheckBox).isSelected()
    editableCheck.setEnabled(item.isEnabled)
    textField.setEnabled(item.isEnabled)
  }
})

Kotlin(手動修正)

enabledCheck.addActionListener {
  val c = SwingUtilities.getAncestorOfClass(JComboBox::class.java, this)
  if (c is JComboBox<*>) {
    val item = c.getItemAt(data.index) as ComboItem
    item.isEnabled = (e.getSource() as JCheckBox).isSelected()
    editableCheck.setEnabled(item.isEnabled)
    textField.setEnabled(item.isEnabled)
  }
}

型引数の省略

  • Java 7以降で可能なジェネリッククラスのコンストラクタ呼び出しに必要な型引数を省略する記法(<>, diamond operator)(Generic Types (The Java™ Tutorials > Learning the Java Language > Generics (Updated)))は、IntelliJの自動変換では正常に変換されない場合がある
  • 例えば、以下のJavaコードのrenderer = new CheckBoxCellRenderer<>();部分は、renderer = CheckBoxCellRenderer<example.CheckBoxNode>()に変換され、Type mismatch: inferred type is CheckBoxCellRenderer<CheckBoxNode> but CheckBoxCellRenderer<E>? was expectedとエラーになる
    • これをrenderer = CheckBoxCellRenderer<>()と空にすると、今度はType expectedとエラーになる
    • Kotlinには<>, diamond operatorは存在せず?、型引数を省略したい場合は何も書く必要がない(Generics: in, out, where - Kotlin Programming Language)
    • renderer = CheckBoxCellRenderer<E>()、またはrenderer = CheckBoxCellRenderer()にすれば正常に動作する
    • 変換元のJavaコードがrenderer = new CheckBoxCellRenderer<E>();なら、IntelliJの自動変換ではrenderer = CheckBoxCellRenderer()になり正常に動作する
  • JList<CheckBoxNode> list2 = new CheckBoxList<>(model);が、なぜかval list2 = CheckBoxList<E>(model)に変換されるバグも存在する?
    • 正しくval list2 = CheckBoxList<CheckBoxNode>(model)に変換される場合もあるが、条件が不明
    • どちらにしても、val list2 = CheckBoxList(model)には変換してくれない

Java

class CheckBoxList<E extends CheckBoxNode> extends JList<E> {
  private transient CheckBoxCellRenderer<E> renderer;
  protected CheckBoxList(ListModel<E> model) {
    super(model);
  }
  @Override public void updateUI() {
    setForeground(null);
    setBackground(null);
    setSelectionForeground(null);
    setSelectionBackground(null);
    removeMouseListener(renderer);
    removeMouseMotionListener(renderer);
    super.updateUI();
    renderer = new CheckBoxCellRenderer<>();
    setCellRenderer(renderer);
    addMouseListener(renderer);
    addMouseMotionListener(renderer);
    putClientProperty("List.isFileList", Boolean.TRUE);
  }
  // ...
}

Kotlin(自動変換)

internal class CheckBoxList<E : CheckBoxNode> protected constructor(model: ListModel<E>) : JList<E>(model) {
  @Transient
  private var renderer: CheckBoxCellRenderer<E>? = null

  override fun updateUI() {
    setForeground(null)
    setBackground(null)
    setSelectionForeground(null)
    setSelectionBackground(null)
    removeMouseListener(renderer)
    removeMouseMotionListener(renderer)
    super.updateUI()
    renderer = CheckBoxCellRenderer<example.CheckBoxNode>()
    // renderer = CheckBoxCellRenderer<CheckBoxNode>() // でもType mismatchでエラー
    setCellRenderer(renderer)
    addMouseListener(renderer)
    addMouseMotionListener(renderer)
    putClientProperty("List.isFileList", java.lang.Boolean.TRUE)
  }
  // ...
}

Kotlin(手動修正)

internal class CheckBoxList<E : CheckBoxNode> protected constructor(model: ListModel<E>) : JList<E>(model) {
  @Transient
  private var renderer: CheckBoxCellRenderer<E>? = null

  override fun updateUI() {
    setForeground(null)
    setBackground(null)
    setSelectionForeground(null)
    setSelectionBackground(null)
    removeMouseListener(renderer)
    removeMouseMotionListener(renderer)
    super.updateUI()
    renderer = CheckBoxCellRenderer<E>()
    // renderer = CheckBoxCellRenderer<>() // NG
    // renderer = CheckBoxCellRenderer() // OK
    setCellRenderer(renderer)
    addMouseListener(renderer)
    addMouseMotionListener(renderer)
    putClientProperty("List.isFileList", java.lang.Boolean.TRUE)
  }
  // ...
}

匿名内部クラス

  • IntelliJの自動変換ではerror: this class does not have a constructor tree.addTreeWillExpandListener(object : TreeWillExpandListener() {とエラーになる
    • IntelliJ 2018.2.5の自動変換では正しく変換されるようになっている
  • インターフェイスから匿名内部クラスのインスタンスを生成する場合は()は不要

Java

tree.addTreeWillExpandListener(new TreeWillExpandListener() {
  @Override public void treeWillExpand(TreeExpansionEvent e)
      throws ExpandVetoException {
    //throw new ExpandVetoException(e, "Tree expansion cancelled");
  }
  @Override public void treeWillCollapse(TreeExpansionEvent e)
      throws ExpandVetoException {
    throw new ExpandVetoException(e, "Tree collapse cancelled");
  }
});

Kotlin(自動変換)

tree.addTreeWillExpandListener(object : TreeWillExpandListener() {
  @Override
  @Throws(ExpandVetoException::class)
  fun treeWillExpand(e: TreeExpansionEvent) {
    //throw new ExpandVetoException(e, "Tree expansion cancelled");
  }
  @Override
  @Throws(ExpandVetoException::class)
  fun treeWillCollapse(e: TreeExpansionEvent) {
    throw ExpandVetoException(e, "Tree collapse cancelled")
  }
})

Kotlin(手動修正)

tree.addTreeWillExpandListener(object : TreeWillExpandListener {
  @Throws(ExpandVetoException::class)
  override fun treeWillExpand(e: TreeExpansionEvent) {
    //throw new ExpandVetoException(e, "Tree expansion cancelled");
  }
  @Throws(ExpandVetoException::class)
  override fun treeWillCollapse(e: TreeExpansionEvent) {
    throw ExpandVetoException(e, "Tree collapse cancelled")
  }
})

ラベル付きReturn

  • 匿名内部クラスからreturnで抜けるJavaコードをIntelliJの自動変換で変換すると、unresolved reference: @decodeとエラーになるKotlinコードに変換される場合がある

Java

JButton decode = new JButton("decode");
decode.addActionListener(e -> {
  String b64 = textArea.getText();
  if (b64.isEmpty()) {
    return;
  }
  try (InputStream is = ...) {
    label.setIcon(new ImageIcon(ImageIO.read(is)));
  } catch (IOException ex) {
    ex.printStackTrace();
  }
});

Kotlin(自動変換)

val decode = JButton("decode")
decode.addActionListener({ e ->
  val b64 = textArea.getText()
  if (b64.isEmpty()) {
    return@decode.addActionListener
  }
  try {
    // ...
  } catch (ex: IOException) {
    ex.printStackTrace()
  }
})

Kotlin(手動修正)

val decode = JButton("decode")
decode.addActionListener {
  val b64 = textArea.getText()
  if (b64.isEmpty()) {
    return@addActionListener
  }
  try {
    // ...
  } catch (ex: IOException) {
    ex.printStackTrace()
  }
}

tryの戻り値

  • Optional.map(...)などからreturnで戻り値を返すJavaコードをIntelliJの自動変換で変換すると、ReturnのラベルがエラーになるKotlinコードに変換される場合がある
    • @Optional.ofNullable(getClass().getResource("unkaku_w.png")).mapがラベルになっている?
  • Kotlinではtryは式で戻り値を持つことが可能なので、ラベル付きReturnは不要
  • Try is an expression - Exceptions: try, catch, finally, throw, Nothing - Kotlin Programming Language

Java

BufferedImage bi = Optional.ofNullable(getClass().getResource("unkaku_w.png"))
    .map(url -> {
      try {
        return ImageIO.read(url);
      } catch (IOException ex) {
        return makeMissingImage();
      }
    }).orElseGet(() -> makeMissingImage());

Kotlin(自動変換)

val bi = Optional.ofNullable(javaClass.getResource("unkaku_w.png")).map({ url ->
  try {
    return@Optional.ofNullable(getClass().getResource("unkaku_w.png")).map ImageIO . read url
  } catch (ex: IOException) {
    return@Optional.ofNullable(getClass().getResource("unkaku_w.png")).map makeMissingImage ()
  }
}).orElseGet({ makeMissingImage() })

Kotlin(手動修正)

val bi = Optional.ofNullable(javaClass.getResource("unkaku_w.png")).map { url ->
  try {
    ImageIO.read(url)
  } catch (ex: IOException) {
    makeMissingImage()
  }
}.orElseGet { makeMissingImage() }

Kotlin(手動修正)

val bi = javaClass.getResource("unkaku_w.png")?.let {
  try {
    ImageIO.read(it)
  } catch (ex: IOException) {
    makeMissingImage()
  }
} ?: makeMissingImage()

メソッド参照

  • IntelliJの自動変換ではメソッド参照の変換が苦手? いくらか修正されている?
    • IntelliJ 2018.2.5の自動変換では、???ではなくPredicate<Component>などに変換されるようになった

Java

public static Stream<TreeNode> children(TreeNode node) {
  Class<TreeNode> clz = TreeNode.class;
  return Collections.list((Enumeration<?>) node.children())
    .stream().filter(clz::isInstance).map(clz::cast);
}

Kotlin(自動変換)

fun children(node: TreeNode): Stream<TreeNode> {
  val clz = TreeNode::class.java
  return Collections.list(node.children() as Enumeration<*>)
    .stream().filter(???({ clz!!.isInstance() })).map(clz!!.cast)
}

Kotlin(手動修正)

fun children(node: TreeNode): Stream<TreeNode> {
  val clz = TreeNode::class.java
  return Collections.list(node.children() as Enumeration<*>)
    .stream().filter(clz::isInstance).map(clz::cast)
    // .filterIsInstance(clz).stream()
}

Kotlin(手動修正): Iterable<*>.filterIsInstance()が便利

fun children(node: TreeNode): List<TreeNode> {
  return (node.children() as Enumeration<*>)
    .toList().filterIsInstance(TreeNode::class.java)
}
  • 例えば.filter(Container.class::isInstance)のようなメソッド参照を、IntelliJ 2018.2.5の自動変換では.filter(Predicate<Component> { Container::class.java!!.isInstance(it) })のように変換するようになった?が、importの追加は対応していない

Java

public static Stream<Component> stream(Container parent) {
  return Stream.of(parent.getComponents())
    .filter(Container.class::isInstance)
    .map(c -> stream(Container.class.cast(c)))
    .reduce(Stream.of(parent), Stream::concat);
  }

Kotlin(自動変換)

// 自動的に以下のimportは生成されないのでunresolved referenceになる
// import java.util.function.BinaryOperator
// import java.util.function.Predicate
fun stream(parent: Container): Stream<Component> {
  return Stream.of(*parent.getComponents())
    .filter(Predicate<Component> { Container::class.java!!.isInstance(it) })
    .map({ c -> stream(Container::class.java!!.cast(c)) })
    .reduce(Stream.of(parent), BinaryOperator<Stream<Component>> { a, b -> Stream.concat(a, b) })
}

Kotlin(手動修正)

import java.util.function.BinaryOperator
import java.util.function.Predicate
fun stream(parent: Container): Stream<Component> {
  // return Stream.of(*parent.getComponents())
  return Arrays.asList(parent.getComponents())
    .filter(Container::class.java::isInstance)
    .map { c -> stream(Container::class.java.cast(c)) }
    // OK: .reduce(Stream.of(parent), BinaryOperator<Stream<Component>>{ a, b -> Stream.concat(a, b) })
    // NG: .reduce(Stream.of(parent), Stream::concat)
    .reduce(Stream.of<Component>(parent), { a, b -> Stream.concat<Component>(a, b) }) // OK
}

Kotlin(手動修正)

// Stream を Iterable に変換してしまう方法もある
fun children(parent: Container): List<Component> {
  return parent.getComponents().toList()
    .filterIsInstance(Container::class.java)
    .map { children(it) }
    .fold(listOf<Component>(parent)) { a, b -> a + b }
}
// ...
children(fileChooser1)
  .filterIsInstance(JTable::class.java)
  // .firstOrNull()?.apply(JTable::removeEditor)
  .firstOrNull()?.let { table ->
    println(table)
    table.removeEditor()
  }

メソッド参照(終端処理の戻り値)

  • Container#add(Component)メソッドは戻り値としてComponentを返す
    • Javaの場合、終端処理のforEach(p::add)では戻り値を無視してContainer#add(Component)のメソッド参照が可能
    • Kotlinの場合、forEach(p::add)とすると、addメソッドで引数がComponent!、戻り値がUnitになるメソッドを探すためメソッド参照で以下のようなエラーになる
hello.kt:38:66: error: none of the following functions can be called with the arguments supplied:
public open fun add(p0: Component!): Component! defined in javax.swing.JPanel
public open fun add(p0: Component!, p1: Any!): Unit defined in javax.swing.JPanel
public open fun add(p0: Component!, p1: Any!, p2: Int): Unit defined in javax.swing.JPanel
public open fun add(p0: Component!, p1: Int): Component! defined in javax.swing.JPanel
public open fun add(p0: PopupMenu!): Unit defined in javax.swing.JPanel
public open fun add(p0: String!, p1: Component!): Component! defined in javax.swing.JPanel
    listOf(button1, button2, button3, button4).forEach(p::add)
                                                          ^

Java

JPanel p = new JPanel(new GridLayout(0, 1, 2, 2));
Arrays.asList(button1, button2).forEach(p::add);

Kotlin(自動変換)

val p = JPanel(GridLayout(0, 1, 2, 2))
Arrays.asList(button1, button2).forEach( ?? ? ({ p.add() }))

Kotlin(手動修正)

val p = JPanel(GridLayout(0, 1, 2, 2))
listOf(button1, button2).forEach { b -> p.add(b) }
//or listOf(button1, button2).map(p::add)

コンストラクタ参照

  • IntelliJの自動変換ではコンストラクタ参照がうまく変換できない場合がある
    • 例えばStream.of("A", "B", "C").map(JToggleButton::new)Stream.of("A", "B", "C").map(Function<String, JToggleButton> { JToggleButton(it) })に変換されて、以下のようなエラーになる
e: App.kt: (13, 34): Interface Function does not have constructors
e: App.kt: (13, 82): Unresolved reference: it
e: App.kt: (13, 89): Cannot choose among the following candidates without completing type inference:
@HidesMembers public inline fun <T> Iterable<???>.forEach(action: (???) -> Unit): Unit defined in kotlin.collections
@HidesMembers public inline fun <K, V> Map<out ???, ???>.forEach(action: (Map.Entry<???, ???>) -> Unit): Unit defined in kotlin.collections
e: App.kt: (13, 99): Cannot infer a type for this parameter. Please specify it explicitly.

Java

Stream.of("A", "B", "C").map(JToggleButton::new).forEach(r -> {
  p.add(r);
  bg.add(r);
});

Kotlin(自動変換)

Stream.of("A", "B", "C").map(Function<String, JToggleButton> { JToggleButton(it) }).forEach({ r ->
  p.add(r)
  bg.add(r)
})

Kotlin(手動修正)

Stream.of("A", "B", "C").map(::JToggleButton).forEach { r ->
  p.add(r)
  bg.add(r)
}

BigDecimal

  • IntelliJの自動変換ではerror: unresolved reference: intValueとエラーになるので、手動でtoInt()に修正する必要がある

Java

double d = delta.y * GRAVITY;
int ia = (int) d;
int ib = (int) Math.floor(d);
int ic = new BigDecimal(d).setScale(0, RoundingMode.DOWN).intValue();
System.out.format("%d %d %d%n", ia, ib, ic);

Kotlin(自動変換)

val d = delta.y * GRAVITY
val ia = d.toInt()
val ib = Math.floor(d) as Int
val ic = BigDecimal(d).setScale(0, RoundingMode.DOWN).intValue()
System.out.format("%d %d %d%n", ia, ib, ic)

Kotlin(手動修正)

val d = delta.y * GRAVITY
val ia = d.toInt()
val ib = Math.floor(d).toInt()
val ic = BigDecimal(d).setScale(0, RoundingMode.DOWN).toInt()
println("${ia} ${ib} ${ic}")

Comparator

  • IntelliJの自動変換ではerror: calls to static methods in Java interfaces are prohibited in JVM target 1.6. Recompile with '-jvm-target 1.8'とエラーになる
    • kotlinc hello.kt -jvm-target 1.8 -include-runtime -d hello.jarkotlincにオプションを追加
  • IntelliJの自動変換ではerror: using 'sort(kotlin.Comparator<in T> /* = java.util.Comparator<in T> */): Unit' is an error. Use sortWith(comparator) instead.とエラーになる
    • list.sort(...)list.sortWith(...)に修正
  • IntelliJの自動変換ではerror: type inference failed: Not enough information to infer parameter T in fun <T : Any!> comparingInt(p0: ((T!) -> Int)!): Comparator<T!>!とエラーになる
    • Comparator.comparingInt({ l -> l.get(0) })Comparator.comparingInt<List<Int>>({ l -> l.get(0) })に修正

Java

import java.util.*;
public class SortTest {
  public static void main(String[] args) {
    List<List<Integer>> list = Arrays.asList(
      Arrays.asList(15, 20, 35),
      Arrays.asList(30, 45, 72),
      Arrays.asList(15, 20, 31),
      Arrays.asList(27, 33, 59),
      Arrays.asList(27, 35, 77));
    list.sort(Comparator.<List<Integer>>comparingInt(l -> l.get(0))
      .thenComparingInt(l -> l.get(1))
      .thenComparingInt(l -> l.get(2))
      .reversed());
    System.out.println(list);
  }
}

Kotlin(自動変換)

fun main(args: Array<String>) {
  val list = Arrays.asList(
    Arrays.asList(15, 20, 35),
    Arrays.asList(30, 45, 72),
    Arrays.asList(15, 20, 31),
    Arrays.asList(27, 33, 59),
    Arrays.asList(27, 35, 77))
  list.sort(Comparator.comparingInt({ l -> l.get(0) })
    .thenComparingInt({ l -> l.get(1) })
    .thenComparingInt({ l -> l.get(2) })
    .reversed())
  System.out.println(list)
}

Kotlin(手動修正)

fun main(args: Array<String>) {
  val list = Arrays.asList(
    Arrays.asList(15, 20, 35),
    Arrays.asList(30, 45, 72),
    Arrays.asList(15, 20, 31),
    Arrays.asList(27, 33, 59),
    Arrays.asList(27, 35, 77))
  list.sortWith(Comparator.comparingInt<List<Int>> { l -> l.get(0) }
    .thenComparingInt { l -> l.get(1) }
    .thenComparingInt { l -> l.get(2) }
    .reversed())
  System.out.println(list)
}

Kotlin(手動修正)

fun main(args: Array<String>) {
  val list = listOf<List<Int>>(
    listOf(15, 20, 35),
    listOf(30, 45, 72),
    listOf(15, 20, 31),
    listOf(27, 33, 59),
    listOf(27, 35, 77))
  val l = list.sortedWith(compareBy({ it.get(0) }, { it.get(1) }, { it.get(2) })).reversed()
  println(l)
}

16進数数値リテラル内のアンダースコア

  • IntelliJの自動変換では、頭に0xを付けて16進数表記した数値リテラルにアンダースコアを付けると、IDE Fatal Errorsになり変換できない
    • IntelliJで自動変換できないだけで、Kotlin16進数表記の数値にアンダースコアを付けても問題なく動作可能
    • アンダースコアがなければ、16進数表記でも問題なくIntelliJで自動変換可能
    • 例1: new Color(0xEE_EE_EE)は、IntelliJの自動変換ではjava.lang.NumberFormatException: For input string: "E_EE_EE"とエラーになる
    • 例2: new Color(0x64_88_AA_AA, true)は、IntelliJの自動変換ではjava.lang.NumberFormatException: For input string: "64_8"とエラーになる

Java

editor1.setSelectionColor(new Color(0x64_88_AA_AA, true)); // IntelliJで自動変換できない
editor2.setSelectionColor(new Color(0x6488AAAA, true)); // IntelliJで自動変換可能

Kotlin(手動修正)

editor1.setSelectionColor(Color(0x64_88_AA_AA, true)) // 問題なし
editor2.setSelectionColor(Color(0x6488AAAA, true)) // 問題なし

int、Int

  • Javaでは例えば-10xFFFFFFFFのような16進数表記でも表現可能だが、Kotlinではerror: the integer literal does not conform to the expected type Intとエラーになるため、IntelliJの自動変換ではマイナス付の表記に自動変換される
  • Java Integer.MAX_VALUE vs Kotlin Int.MAX_VALUE - Stack Overflow

Java

Color argb = new Color(0xffaabbcc, true);
Color rgb = new Color(0xaabbcc);

Kotlin(自動変換)

val argb = Color(-0x554434, true)
val rgb = Color(0xaabbcc)

Kotlin(手動修正)

// LongからIntに変換する方法もある
val argb = Color(0xff_aa_bb_cc.toInt(), true)
println(argb == Color(-0x554434, true)) // true

@SuppressWarnings

  • IntelliJの自動変換では、@SuppressWarnings("unchecked")などは削除されるので、Unchecked cast: WatchEvent<*>! to WatchEvent<Path>と警告される
    • 手動で@Suppress("UNCHECKED_CAST")などを付ける必要がある

Java

@SuppressWarnings("unchecked")
WatchEvent<Path> ev = (WatchEvent<Path>) event;

Kotlin(手動修正)

@Suppress("UNCHECKED_CAST")
val ev = event as WatchEvent<Path>

手動修正が不要なIntelliJの自動変換

  • IntelliJの自動変換で変換後の手動修正が不要なケースのメモ

セーフキャスト

Java

Set<?> f = v instanceof Set ? (Set<?>) v : EnumSet.noneOf(Permissions.class);

Kotlin

val f = v as? Set<*> ?: EnumSet.noneOf(Permissions::class.java)

明示的に外側のクラス名で修飾されたthis

Java

class FileListTable extends JTable {
  private class RubberBandingListener extends MouseAdapter {
    @Override public void mousePressed(MouseEvent e) {
      FileListTable table = FileListTable.this
      // ...

Kotlin

class FileListTable(model: TableModel) : JTable(model) {
  private inner class RubberBandingListener : MouseAdapter() {
    override fun mousePressed(e: MouseEvent) {
      val table = this@FileListTable
      // ...

Swing + Kotlin サンプル

SwingUtilities.getAncestorOfClass(...)

Java

Container p = SwingUtilities.getAncestorOfClass(JViewport.class, this);
if (!(p instanceof JViewport)) {
  return;
}

Kotlin

val p = SwingUtilities.getAncestorOfClass(JViewport::class.java, this) as? JViewport ?: return

JTable

  • Class<?>Class<Any>Class<Object>Class<*>Class<out Any>に変換した方が良いかもしれない
    • IntelliJの自動変換ではClass<*>
  • Object#getClass()o.javaClass
  • オーバーライドの方法、@Overrideoverride
  • 配列、二次元配列
  • apply
  • switchwhen
  • DefaultTableModel#getColumnClass(...)メソッドをオーバーライドして、Boolean::class.javaを返してもセルレンダラーにJCheckBoxは適用されない
    • IntelliJの自動変換ではBoolean::class.javaになるが、java.lang.Boolean::class.javaに修正する必要がある

Java

import java.awt.*;
import javax.swing.*;
import javax.swing.table.*;

public class JTableExample {
  public JComponent makeUI() {
    String[] cn = {"String", "Integer", "Boolean"};
    Object[][] data = {
      {"aaa", 12, true}, {"bbb", 5, false},
      {"CCC", 92, true}, {"DDD", 0, false}
    };
    TableModel m = new DefaultTableModel(data, cn) {
      @Override public Class<?> getColumnClass(int col) {
        return getValueAt(0, col).getClass();
      }
    };
    JTable table = new JTable(m);
    table.setAutoCreateRowSorter(true);

    JPanel p = new JPanel(new BorderLayout());
    p.add(new JScrollPane(table));
    return p;
  }
  public static void main(String... args) {
    EventQueue.invokeLater(() -> {
      JFrame f = new JFrame();
      f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      f.add(new JTableExample().makeUI());
      f.setSize(320, 240);
      f.setLocationRelativeTo(null);
      f.setVisible(true);
    });
  }
}

Kotlin

import java.awt.*
import javax.swing.*
import javax.swing.table.*

fun makeUI(): JComponent {
  val cn = arrayOf("String", "Integer", "Boolean")
  val data = arrayOf(
    arrayOf("aaa", 12, true), arrayOf("bbb", 5, false),
    arrayOf("CCC", 92, true), arrayOf("DDD", 0, false))
  val m = object: DefaultTableModel(data, cn) {
    override fun getColumnClass(col: Int): Class<Any> {
      return getValueAt(0, col).javaClass
    }
  }
  // val m = object: DefaultTableModel(data, cn) {
  //   override fun getColumnClass(col: Int): Class<*> {
  //     return when (col) {
  //       0 -> String::class.java
  //       1 -> Integer::class.java
  //       // XXX: 2 -> Boolean::class.java
  //       2 -> java.lang.Boolean::class.java
  //       else -> Object::class.java
  //     }
  //   }
  // }
  val table = JTable(m).apply {
    autoCreateRowSorter = true
  }
  return JPanel(BorderLayout(5, 5)).apply {
    add(JScrollPane(table))
  }
}
fun main(args: Array<String>) {
  EventQueue.invokeLater {
    JFrame("kotlin swing").apply {
      defaultCloseOperation = JFrame.EXIT_ON_CLOSE
      add(makeUI())
      size = Dimension(320, 240)
      setLocationRelativeTo(null)
      setVisible(true)
    }
  }
}

JTree

Java

import java.awt.*;
import java.util.*;
import javax.swing.*;

public class JTreeExample {
  public JComponent makeUI() {
    JTree tree = new JTree();

    JButton b1 = new JButton("expand");
    b1.addActionListener(e -> expandAll(tree));

    JButton b2 = new JButton("collapse");
    b2.addActionListener(e -> collapseAll(tree));

    JPanel pp = new JPanel(new GridLayout(1, 0, 5, 5));
    Arrays.asList(b1, b2).forEach(pp::add);
    JPanel p = new JPanel(new BorderLayout(5, 5));
    p.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
    p.add(new JScrollPane(tree));
    p.add(pp, BorderLayout.SOUTH);
    return p;
  }
  protected static void expandAll(JTree tree) {
    int row = 0;
    while (row < tree.getRowCount()) {
      tree.expandRow(row);
      row++;
    }
  }
  protected static void collapseAll(JTree tree) {
    int row = tree.getRowCount() - 1;
    while (row >= 0) {
      tree.collapseRow(row);
      row--;
    }
  }
  public static void main(String[] args) {
    EventQueue.invokeLater(() -> {
      JFrame f = new JFrame();
      f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      f.add(new JTreeExample().makeUI());
      f.setSize(320, 240);
      f.setLocationRelativeTo(null);
      f.setVisible(true);
    });
  }
}

Kotlin

import java.awt.*
import javax.swing.*

fun makeUI(): JComponent {
  val tree = JTree()
  val p = JPanel(GridLayout(1, 0, 5, 5)).apply {
    add(JButton("expand").apply {
      addActionListener { expandAll(tree) }
    })
    add(JButton("collapse").apply {
      addActionListener { collapseAll(tree) }
    })
  }
  return JPanel(BorderLayout(5, 5)).apply {
    setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5))
    add(JScrollPane(tree))
    add(p, BorderLayout.SOUTH)
  }
}
fun expandAll(tree: JTree) {
  var row = 0
  while (row < tree.getRowCount()) {
    tree.expandRow(row)
    row++
  }
}
fun collapseAll(tree : JTree) {
  var row = tree.getRowCount() - 1
  while (row >= 0) {
    tree.collapseRow(row)
    row--
  }
}
fun main(args: Array<String>) {
  EventQueue.invokeLater {
    JFrame("kotlin swing").apply {
      defaultCloseOperation = JFrame.EXIT_ON_CLOSE
      add(makeUI())
      size = Dimension(320, 240)
      setLocationRelativeTo(null)
      setVisible(true)
    }
  }
}

JCheckBox

  • Smart Cast、キャストでifのネストが深くなるのを避けたい
    • イベント発生元がAbstractButtonなのは自明なのでasを使用する
      • val b = e.getSource() as AbstractButton
    • 以下のような場合は、applyでも回避可能

Java

import java.awt.*;
import javax.swing.*;

public class JCheckBoxExample {
  public JComponent makeUI() {
    JCheckBox cb = new JCheckBox("Always On Top", true);
    cb.addActionListener(e -> {
      AbstractButton b = (AbstractButton) e.getSource();
      Container c = b.getTopLevelAncestor();
      if (c instanceof Window) {
        ((Window) c).setAlwaysOnTop(b.isSelected());
      }
    });
    JPanel p = new JPanel(new BorderLayout());
    p.add(cb, BorderLayout.NORTH);
    return p;
  }
  public static void main(String[] args) {
    EventQueue.invokeLater(() -> {
      JFrame f = new JFrame();
      f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
      f.add(new JCheckBoxExample().makeUI());
      f.setSize(320, 240);
      f.setLocationRelativeTo(null);
      f.setVisible(true);
    });
  }
}

Kotlin

import java.awt.*
import javax.swing.*

fun makeUI(): JComponent {
  val cb = JCheckBox("Always On Top", true).apply {
//     addActionListener { e ->
//       val c = e.getSource()
//       if (c is AbstractButton) {
//         val w = c.getTopLevelAncestor()
//         if (w is Window) {
//           w.setAlwaysOnTop(c.isSelected())
//         }
//       }
    addActionListener { e ->
      val b = e.getSource() as AbstractButton
      val w = b.getTopLevelAncestor()
      if (w is Window) {
        w.setAlwaysOnTop(b.isSelected())
      }
    }
//     addActionListener {
//       val w = getTopLevelAncestor()
//       if (w is Window) {
//           w.setAlwaysOnTop(isSelected())
//       }
//     }
  }
  return JPanel(BorderLayout()).apply {
    add(cb, BorderLayout.NORTH)
  }
}

fun main(args: Array<String>) {
  EventQueue.invokeLater {
    JFrame("kotlin swing").apply {
      defaultCloseOperation = JFrame.EXIT_ON_CLOSE
      add(makeUI())
      size = Dimension(320, 240)
      setLocationRelativeTo(null)
      setVisible(true)
    }
  }
}

TableCellEditor

  • extends, implements
    • IntelliJの自動変換では勝手にopenは付けないようなので、以下のようなエラーが出る場合は、internal class CheckBoxesPanel : JPanel() {open class CheckBoxesPanel : JPanel() {に修正する
      MainPanel.kt:81:37: error: this type is final, so it cannot be inherited from
      internal class CheckBoxesRenderer : CheckBoxesPanel(), TableCellRenderer {
      
  • IntelliJの自動変換ではstatic変数がうまく変換できない?
    • 手動でenum classにして回避
      companion object {
        protected val TITLES = arrayOf("r", "w", "x")
      }
      //...
      MainPanel.kt:45:5: error: property must be initialized or be abstract
          var buttons: Array<JCheckBox>
          ^
      MainPanel.kt:55:19: error: type mismatch: inferred type is Array<JCheckBox?> but Array<JCheckBox> was expected
              buttons = arrayOfNulls<JCheckBox>(TITLES.size)
      

Java

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.plaf.*;
import javax.swing.table.*;

public final class MainPanel {
  public JComponent makeUI() {
    String[] columnNames = {"user", "rwx"};
    Object[][] data = {
      {"owner", 7}, {"group", 6}, {"other", 5}
    };
    TableModel model = new DefaultTableModel(data, columnNames) {
      @Override public Class<?> getColumnClass(int column) {
        return getValueAt(0, column).getClass();
      }
    };
    JTable table = new JTable(model) {
      @Override public void updateUI() {
        super.updateUI();
        getColumnModel().getColumn(1).setCellRenderer(new CheckBoxesRenderer());
        getColumnModel().getColumn(1).setCellEditor(new CheckBoxesEditor());
      }
    };
    table.putClientProperty("terminateEditOnFocusLost", Boolean.TRUE);

    JPanel p = new JPanel(new BorderLayout());
    p.add(new JScrollPane(table));
    return p;
  }
  public static void main(String[] args) {
    EventQueue.invokeLater(() -> {
      JFrame frame = new JFrame();
      frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
      frame.getContentPane().add(new MainPanel().makeUI());
      frame.setSize(320, 240);
      frame.setLocationRelativeTo(null);
      frame.setVisible(true);
    });
  }
}

class CheckBoxesPanel extends JPanel {
  protected static final String[] TITLES = {"r", "w", "x"};
  public JCheckBox[] buttons;
  @Override public void updateUI() {
    super.updateUI();
    setOpaque(false);
    setBackground(new Color(0x0, true));
    setLayout(new BoxLayout(this, BoxLayout.X_AXIS));
    initButtons();
  }
  private void initButtons() {
    buttons = new JCheckBox[TITLES.length];
    for (int i = 0; i < buttons.length; i++) {
      JCheckBox b = new JCheckBox(TITLES[i]);
      b.setOpaque(false);
      b.setFocusable(false);
      b.setRolloverEnabled(false);
      b.setBackground(new Color(0x0, true));
      buttons[i] = b;
      add(b);
      add(Box.createHorizontalStrut(5));
    }
  }
  protected void updateButtons(Object v) {
    removeAll();
    initButtons();
    Integer i = v instanceof Integer ? (Integer) v : 0;
    buttons[0].setSelected((i & (1 << 2)) != 0);
    buttons[1].setSelected((i & (1 << 1)) != 0);
    buttons[2].setSelected((i & (1 << 0)) != 0);
  }
}

class CheckBoxesRenderer extends CheckBoxesPanel implements TableCellRenderer {
  @Override public void updateUI() {
    super.updateUI();
    setName("Table.cellRenderer");
  }
  @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
    updateButtons(value);
    return this;
  }
}

class CheckBoxesEditor extends AbstractCellEditor implements TableCellEditor {
  private final CheckBoxesPanel panel = new CheckBoxesPanel() {
    @Override public void updateUI() {
      super.updateUI();
      EventQueue.invokeLater(() -> {
        ActionMap am = getActionMap();
        for (int i = 0; i < buttons.length; i++) {
          String title = TITLES[i];
          am.put(title, new AbstractAction(title) {
            @Override public void actionPerformed(ActionEvent e) {
              Arrays.stream(buttons)
              .filter(b -> b.getText().equals(title))
              .findFirst()
              .ifPresent(JCheckBox::doClick);
              fireEditingStopped();
            }
          });
        }
        InputMap im = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0), TITLES[0]);
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0), TITLES[1]);
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, 0), TITLES[2]);
      });
    }
  };
  @Override public Component getTableCellEditorComponent(JTable table, Object value, boolean isSelected, int row, int column) {
    panel.updateButtons(value);
    return panel;
  }
  @Override public Object getCellEditorValue() {
    int i = 0;
    i = panel.buttons[0].isSelected() ? 1 << 2 | i : i;
    i = panel.buttons[1].isSelected() ? 1 << 1 | i : i;
    i = panel.buttons[2].isSelected() ? 1 << 0 | i : i;
    return i;
  }
}

Kotlin

import java.awt.*
import java.awt.event.*
import java.util.*
import javax.swing.*
import javax.swing.event.*
import javax.swing.plaf.*
import javax.swing.table.*

fun makeUI(): JComponent {
  val columnNames = arrayOf("user", "rwx")
  val data = arrayOf(arrayOf<Any>("owner", 7), arrayOf<Any>("group", 6), arrayOf<Any>("other", 5))
  val model = object : DefaultTableModel(data, columnNames) {
    override fun getColumnClass(column: Int): Class<*> {
      return getValueAt(0, column).javaClass
    }
  }
  val table = object : JTable(model) {
    override fun updateUI() {
      super.updateUI()
      getColumnModel().getColumn(1).setCellRenderer(CheckBoxesRenderer())
      getColumnModel().getColumn(1).setCellEditor(CheckBoxesEditor())
    }
  }
  table.putClientProperty("terminateEditOnFocusLost", java.lang.Boolean.TRUE)
  return JPanel(BorderLayout(5, 5)).apply {
    add(JScrollPane(table))
  }
}

fun main(args: Array<String>) {
  EventQueue.invokeLater {
    JFrame("kotlin swing").apply {
      defaultCloseOperation = JFrame.EXIT_ON_CLOSE
      add(makeUI())
      size = Dimension(320, 240)
      setLocationRelativeTo(null)
      setVisible(true)
    }
  }
}

open class CheckBoxesPanel() : JPanel() {
  public val buttons = arrayOf(JCheckBox(Permission.READ.toString()), JCheckBox(Permission.WRITE.toString()), JCheckBox(Permission.EXECUTE.toString()))

  override fun updateUI() {
    super.updateUI()
    setOpaque(false)
    setBackground(Color(0x0, true))
    setLayout(BoxLayout(this, BoxLayout.X_AXIS))
    EventQueue.invokeLater({ initButtons() })
  }

  private fun initButtons() {
    for (b in buttons) {
      b.setOpaque(false)
      b.setFocusable(false)
      b.setRolloverEnabled(false)
      b.setBackground(Color(0x0, true))
      add(b)
      add(Box.createHorizontalStrut(5))
    }
  }

  public fun updateButtons(v: Any) {
    removeAll()
    initButtons()
    val i = v as ? Int ? : 0
    buttons[0].setSelected(i and (1 shl 2) != 0)
    buttons[1].setSelected(i and (1 shl 1) != 0)
    buttons[2].setSelected(i and (1 shl 0) != 0)
  }
}

enum class Permission(var str: String) {
  READ("r"), WRITE("w"), EXECUTE("x");
  override fun toString() = str
}

internal class CheckBoxesRenderer : CheckBoxesPanel(), TableCellRenderer {
  override fun updateUI() {
    super.updateUI()
    setName("Table.cellRenderer")
  }
  override fun getTableCellRendererComponent(table: JTable, value: Any, isSelected: Boolean, hasFocus: Boolean, row: Int, column: Int): Component {
    updateButtons(value)
    return this
  }
}

internal class CheckBoxesEditor : AbstractCellEditor(), TableCellEditor {
  private val panel = object : CheckBoxesPanel() {
    override fun updateUI() {
      super.updateUI()
      EventQueue.invokeLater({
        val am = getActionMap()
        for (i in buttons.indices) {
          val title = buttons[i].getText()
          am.put(title, object : AbstractAction(title) {
            override fun actionPerformed(e: ActionEvent) {
              Arrays.stream(buttons)
                .filter({ b -> b.getText() == title })
                .findFirst()
                .ifPresent({ it.doClick() })
              fireEditingStopped()
            }
          })
        }
        val im = getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_R, 0), Permission.READ.toString())
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_W, 0), Permission.WRITE.toString())
        im.put(KeyStroke.getKeyStroke(KeyEvent.VK_X, 0), Permission.EXECUTE.toString())
      })
    }
  }

  override fun getTableCellEditorComponent(table: JTable, value: Any, isSelected: Boolean, row: Int, column: Int): Component {
    panel.updateButtons(value)
    return panel
  }

  override fun getCellEditorValue(): Any {
    var i = 0
    i = if (panel.buttons[0].isSelected()) 1 shl 2 or i else i
    i = if (panel.buttons[1].isSelected()) 1 shl 1 or i else i
    i = if (panel.buttons[2].isSelected()) 1 shl 0 or i else i
    return i
  }
}

ActionListener

  • IntelliJの自動変換ではActionListenerなどの関数型インターフェースのラムダ式をうまく変換できない場合がある
    • AbstractCellEditor#stopCellEditing()booleanを返すので、error: type mismatch: inferred type is (???) -> Boolean but ActionListener? was expectedとエラーになる
  • SAM Conversions - Calling Java from Kotlin - Kotlin Programming Language

Java

protected ActionListener handler;
//...
handler = e -> stopCellEditing();

Kotlin(自動変換)

protected var handler: ActionListener? = null
//...
handler = { e -> stopCellEditing() }
// error: type mismatch: inferred type is (???) -> Boolean but ActionListener? was expected
//             handler = { e -> stopCellEditing() }
// error: cannot infer a type for this parameter. Please specify it explicitly.
//             handler = { e -> stopCellEditing() }
// error: modifier 'override' is not applicable to 'local function'
//               override fun actionPerformed(e: ActionEvent) : Unit {

Kotlin(手動修正)

protected var handler: ActionListener? = null
//...
handler = ActionListener { stopCellEditing() }
// or:
handler = object: ActionListener {
  override fun actionPerformed(e: ActionEvent) : Unit { // Unitは無くても可
    stopCellEditing()
  }
}
  • IntelliJの自動変換で、以下のようなエラーになる場合は、ObjectAnyに手動変換する
    error: class 'CheckBoxNodeRenderer' is not abstract and does not implement abstract member public abstract fun getTreeCellRendererComponent(p0: JTree!, p1: Any!, p2: Boolean, p3: Boolean, p4: Boolean, p5: Int, p6: Boolean): Component! defined in javax.swing.tree.TreeCellRenderer
    internal class CheckBoxNodeRenderer : TreeCellRenderer {
  • getterのみオーバーライド

Java

class CheckBoxNodeEditor extends AbstractCellEditor implements TreeCellEditor {
  @Override public Object getCellEditorValue() {
    return new CheckBoxNode(checkBox.getText(), checkBox.isSelected());
  }
  //...

Kotlin(自動変換)

val cellEditorValue: Any
  override get() = CheckBoxNode(checkBox.getText(), checkBox.isSelected())
//error: modifier 'override' is not applicable to 'getter'
//       override get() = CheckBoxNode(checkBox.getText(), checkBox.isSelected())

Kotlin(手動修正)

  override fun getCellEditorValue(): Any {
    return CheckBoxNode(checkBox.getText(), checkBox.isSelected())
  }

Java

import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import javax.swing.tree.*;

public class LeafCheckBoxTreeTest {
  private JComponent makeUI() {
    JTree tree = new JTree();
    tree.setEditable(true);
    tree.setCellRenderer(new CheckBoxNodeRenderer());
    tree.setCellEditor(new CheckBoxNodeEditor());
    tree.setRowHeight(18);

    boolean b = true;
    TreeModel model = tree.getModel();
    DefaultMutableTreeNode root = (DefaultMutableTreeNode) model.getRoot();
    Enumeration<?> e = root.breadthFirstEnumeration();
    while (e.hasMoreElements()) {
      DefaultMutableTreeNode node = (DefaultMutableTreeNode) e.nextElement();
      String s = Objects.toString(node.getUserObject(), "");
      node.setUserObject(new CheckBoxNode(s, b));
      b ^= true;
    }
    return new JScrollPane(tree);
  }
  public static void main(String[] args) {
    EventQueue.invokeLater(() -> {
      UIManager.put("swing.boldMetal", Boolean.FALSE);
      JFrame f = new JFrame();
      f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
      f.getContentPane().add(new LeafCheckBoxTreeTest().makeUI());
      f.setSize(320, 240);
      f.setLocationRelativeTo(null);
      f.setVisible(true);
    });
  }
}

class CheckBoxNodeRenderer implements TreeCellRenderer {
  private final JCheckBox checkBox = new JCheckBox();
  private final DefaultTreeCellRenderer renderer = new DefaultTreeCellRenderer();
  @Override public Component getTreeCellRendererComponent(
      JTree tree, Object value, boolean selected, boolean expanded,
      boolean leaf, int row, boolean hasFocus) {
    if (leaf && value instanceof DefaultMutableTreeNode) {
      checkBox.setEnabled(tree.isEnabled());
      checkBox.setFont(tree.getFont());
      checkBox.setOpaque(false);
      checkBox.setFocusable(false);
      Object userObject = ((DefaultMutableTreeNode) value).getUserObject();
      if (userObject instanceof CheckBoxNode) {
        CheckBoxNode node = (CheckBoxNode) userObject;
        checkBox.setText(node.text);
        checkBox.setSelected(node.selected);
      }
      return checkBox;
    }
    return renderer.getTreeCellRendererComponent(
        tree, value, selected, expanded, leaf, row, hasFocus);
  }
}

class CheckBoxNodeEditor extends AbstractCellEditor implements TreeCellEditor {
  private final JCheckBox checkBox = new JCheckBox() {
    protected transient ActionListener handler;
    @Override public void updateUI() {
      removeActionListener(handler);
      super.updateUI();
      setOpaque(false);
      setFocusable(false);
      handler = e -> stopCellEditing();
      addActionListener(handler);
    }
  };
  @Override public Component getTreeCellEditorComponent(
      JTree tree, Object value, boolean selected, boolean expanded,
      boolean leaf, int row) {
    if (leaf && value instanceof DefaultMutableTreeNode) {
      Object userObject = ((DefaultMutableTreeNode) value).getUserObject();
      if (userObject instanceof CheckBoxNode) {
        checkBox.setSelected(((CheckBoxNode) userObject).selected);
      } else {
        checkBox.setSelected(false);
      }
      checkBox.setText(value.toString());
    }
    return checkBox;
  }
  @Override public Object getCellEditorValue() {
    return new CheckBoxNode(checkBox.getText(), checkBox.isSelected());
  }
  @Override public boolean isCellEditable(EventObject e) {
    if (e instanceof MouseEvent && e.getSource() instanceof JTree) {
      MouseEvent me = (MouseEvent) e;
      JTree tree = (JTree) me.getComponent();
      TreePath path = tree.getPathForLocation(me.getX(), me.getY());
      Object o = path.getLastPathComponent();
      if (o instanceof TreeNode) {
        return ((TreeNode) o).isLeaf();
      }
    }
    return false;
  }
}

class CheckBoxNode {
  public final String text;
  public final boolean selected;
  protected CheckBoxNode(String text, boolean selected) {
    this.text = text;
    this.selected = selected;
  }
  @Override public String toString() {
    return text;
  }
}

Kotlin

import java.awt.*
import java.awt.event.*
import java.util.*
import javax.swing.*
import javax.swing.event.*
import javax.swing.tree.*

private fun makeUI(): JComponent {
  val tree = JTree()
  tree.setEditable(true)
  tree.setCellRenderer(CheckBoxNodeRenderer())
  tree.setCellEditor(CheckBoxNodeEditor())
  tree.setRowHeight(18)

  var b = true
  val model = tree.getModel()
  val root = model.getRoot() as DefaultMutableTreeNode
  val e = root.breadthFirstEnumeration()
  while (e.hasMoreElements()) {
    val node = e.nextElement() as DefaultMutableTreeNode
    val s = Objects.toString(node.getUserObject(), "")
    node.setUserObject(CheckBoxNode(s, b))
    b = b xor true
  }
  return JScrollPane(tree)
}

fun main(args: Array<String>) {
  EventQueue.invokeLater {
    UIManager.put("swing.boldMetal", false)
    JFrame("kotlin swing").apply {
      defaultCloseOperation = JFrame.EXIT_ON_CLOSE
      add(makeUI())
      size = Dimension(320, 240)
      setLocationRelativeTo(null)
      setVisible(true)
    }
  }
}

internal class CheckBoxNodeRenderer : TreeCellRenderer {
  private val checkBox = JCheckBox()
  private val renderer = DefaultTreeCellRenderer()
  override fun getTreeCellRendererComponent(
    tree: JTree, value: Any, selected: Boolean, expanded: Boolean,
    leaf: Boolean, row: Int, hasFocus: Boolean): Component {
    if (leaf && value is DefaultMutableTreeNode) {
      checkBox.setEnabled(tree.isEnabled())
      checkBox.setFont(tree.getFont())
      checkBox.setOpaque(false)
      checkBox.setFocusable(false)
      val userObject = value.getUserObject()
      if (userObject is CheckBoxNode) {
        val node = userObject // as CheckBoxNode
        checkBox.setText(node.text)
        checkBox.setSelected(node.selected)
      }
      return checkBox
    }
    return renderer.getTreeCellRendererComponent(
      tree, value, selected, expanded, leaf, row, hasFocus)
  }
}

internal class CheckBoxNodeEditor : AbstractCellEditor(), TreeCellEditor {
  private val checkBox = object : JCheckBox() {
    protected var handler: ActionListener? = null
    override fun updateUI() {
      removeActionListener(handler)
      super.updateUI()
      setOpaque(false)
      setFocusable(false)
      // handler = object: ActionListener {
      //   override fun actionPerformed(e: ActionEvent) : Unit {
      //     stopCellEditing()
      //   }
      // }
      handler = ActionListener { stopCellEditing() }
      addActionListener(handler)
    }
  }

  override fun getTreeCellEditorComponent(
      tree: JTree, value: Any, selected: Boolean, expanded: Boolean,
      leaf: Boolean, row: Int): Component {
    if (leaf && value is DefaultMutableTreeNode) {
      val userObject = value.getUserObject()
      if (userObject is CheckBoxNode) {
        checkBox.setSelected(userObject.selected)
      } else {
        checkBox.setSelected(false)
      }
      checkBox.setText(value.toString())
    }
    return checkBox
  }

  override fun getCellEditorValue(): Any {
    return CheckBoxNode(checkBox.getText(), checkBox.isSelected())
  }

  override fun isCellEditable(e: EventObject): Boolean {
    if (e is MouseEvent && e.getSource() is JTree) {
      val tree = e.getComponent() as JTree
      val path = tree.getPathForLocation(e.getX(), e.getY())
      val o = path.getLastPathComponent()
      if (o is TreeNode) {
        return o.isLeaf()
      }
    }
    return false
  }
}

internal class CheckBoxNode public constructor(val text: String, val selected: Boolean) {
  override fun toString(): String {
    return text
  }
}

Optional

  • Optional#orElse(...)は、let {...} ?: ...に変換可能
    • Integer stepSize = Optional.ofNullable(stepSizeMap.get(calendarField)).orElse(1);
    • val stepSize = stepSizeMap.get(calendarField) ?: 1
  • 例えばSpinnerDateModel#getNextValue(...)メソッドをoverride fun getNextValue(): Object = Calendar.getInstance().apply { ...のように変換すると、error: type mismatch: inferred type is Date! but Object was expectedとエラーになる
  • override fun getNextValue(): Any = Calendar.getInstance().apply { ...Anyを使用するようオーバーライドする必要がある
  • Date!は、DateもしくはDate?の意味

Java

import java.awt.*;
import java.text.*;
import java.util.*;
import javax.swing.*;
import javax.swing.text.*;

public class OptionalExample {
  private JComponent makeUI() {
    SimpleDateFormat format = new SimpleDateFormat(
        "mm:ss, SSS", Locale.getDefault());
    DefaultFormatterFactory factory = new DefaultFormatterFactory(
        new DateFormatter(format));

    Calendar calendar = Calendar.getInstance();
    calendar.set(Calendar.HOUR_OF_DAY, 0);
    calendar.clear(Calendar.MINUTE);
    calendar.clear(Calendar.SECOND);
    calendar.clear(Calendar.MILLISECOND);
    Date d = calendar.getTime();

    JSpinner sp1 = new JSpinner(new SpinnerDateModel(
        d, null, null, Calendar.SECOND));
    JSpinner.DefaultEditor ed1 = (JSpinner.DefaultEditor) sp1.getEditor();
    ed1.getTextField().setFormatterFactory(factory);

    HashMap<Integer, Integer> stepSizeMap = new HashMap<>();
    stepSizeMap.put(Calendar.HOUR_OF_DAY, 1);
    stepSizeMap.put(Calendar.MINUTE,      1);
    stepSizeMap.put(Calendar.SECOND,      30);
    stepSizeMap.put(Calendar.MILLISECOND, 500);

    JSpinner sp2 = new JSpinner(new SpinnerDateModel(
        d, null, null, Calendar.SECOND) {
      @Override public Object getPreviousValue() {
        Calendar cal = Calendar.getInstance();
        cal.setTime(getDate());
        Integer calendarField = getCalendarField();
        Integer stepSize = Optional.ofNullable(
            stepSizeMap.get(calendarField)).orElse(1);
        cal.add(calendarField, -stepSize);
        return cal.getTime();
      }
      @Override public Object getNextValue() {
        Calendar cal = Calendar.getInstance();
        cal.setTime(getDate());
        Integer calendarField = getCalendarField();
        Integer stepSize = Optional.ofNullable(
            stepSizeMap.get(calendarField)).orElse(1);
        cal.add(calendarField, stepSize);
        return cal.getTime();
      }
    });
    JSpinner.DefaultEditor ed2 = (JSpinner.DefaultEditor) sp2.getEditor();
    ed2.getTextField().setFormatterFactory(factory);

    JPanel p = new JPanel();
    p.add(sp1);
    p.add(sp2);
    return p;
  }
  public static void main(String... args) {
    EventQueue.invokeLater(() -> {
      JFrame f = new JFrame();
      f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
      f.getContentPane().add(new OptionalExample().makeUI());
      f.setSize(320, 240);
      f.setLocationRelativeTo(null);
      f.setVisible(true);
    });
  }
}

Kotlin

import java.awt.*
import java.text.*
import java.util.*
import javax.swing.*
import javax.swing.text.*

fun makeUI(): JComponent {
  val format = SimpleDateFormat("mm:ss, SSS", Locale.getDefault())
  val factory = DefaultFormatterFactory(DateFormatter(format))

  val calendar = Calendar.getInstance().apply {
    set(Calendar.HOUR_OF_DAY, 0)
    clear(Calendar.MINUTE)
    clear(Calendar.SECOND)
    clear(Calendar.MILLISECOND)
  }
  val d = calendar.getTime()

  val sp1 = JSpinner(SpinnerDateModel(d, null, null, Calendar.SECOND))
  val ed1 = sp1.getEditor()
  if (ed1 is JSpinner.DefaultEditor) {
    ed1.getTextField().setFormatterFactory(factory)
  }

  val stepSizeMap: HashMap<Int, Int> = hashMapOf(
      Calendar.HOUR_OF_DAY to 1,
      Calendar.MINUTE      to 1,
      Calendar.SECOND      to 30,
      Calendar.MILLISECOND to 500)

  val sp2 = JSpinner(object: SpinnerDateModel(d, null, null, Calendar.SECOND) {
    override fun getPreviousValue(): Any = Calendar.getInstance().apply {
      time = getDate()
      val stepSize = stepSizeMap.get(calendarField).let { it } ?: 1
      add(calendarField, -stepSize)
    }.getTime()
    override fun getNextValue(): Any = Calendar.getInstance().apply {
        setTime(getDate())
        val stepSize = stepSizeMap.get(calendarField).let { it } ?: 1
        add(calendarField, stepSize)
    }.getTime()
  })
  val ed2 = sp2.getEditor()
  if (ed2 is JSpinner.DefaultEditor) {
    ed2.getTextField().setFormatterFactory(factory)
  }

  return JPanel().apply {
    add(sp1)
    add(sp2)
  }
}

fun main(args: Array<String>) {
  EventQueue.invokeLater {
    JFrame("kotlin swing").apply {
      defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
      add(makeUI())
      size = Dimension(320, 240)
      setLocationRelativeTo(null)
      setVisible(true)
    }
  }
}

SwingWorker

JComponent#updateUI()

  • SwingコンポーネントのUIプロパティを、現在のLookAndFeelで設定されている値にリセットするupdateUI()メソッドは、コンストラクタから呼び出されるので、インスタンス変数が初期化されるまえに実行されることがある
  • このため、Kotlinのコードに変換したupdateUI()メソッド内でnullチェックを行うと、不要なnullチェックをしているとか、!= nullが常にtrueであると警告される
  • nullチェックを外すとNullPointerExceptionになる
  • OptionalObjects.nonNull(...)で、警告をごまかす方法もある

Java

public final class MainPanel extends JPanel {
  private final JSpinner spinner = new JSpinner();
  @Override public void updateUI() {
    super.updateUI();
    Long lv = Optional.ofNullable(spinner)
        .map(s -> (Long) s.getModel().getValue())
        .orElse(1000L);
    // Long lv = spinner != null ? (Long) spinner.getModel().getValue() : 1000L;
    // ...
  }

Kotlin

class MainPanel : JPanel(BorderLayout()) {
  private val spinner = JSpinner()
  private val spinner: JSpinner? = JSpinner()
  override fun updateUI() {
    super.updateUI()
    val lv = Optional.ofNullable(spinner)
        .map { it.getModel().getValue() }
        .orElse(1000L)

    // Unnecessary safe call on a non-null receiver of type JSpinner
    // val lv = spinner
    //     ?.let { it.getModel().getValue() }
    //     ?: 1000L

    // NullPointerException
    // val lv = spinner.getModel().getValue()

    // Condition 'spinner != null' is always 'true'
    // val lv = if (spinner != null) spinner.getModel().getValue() else 1000L

    // private val spinner: JSpinner? = JSpinner() なら(他の個所もspinner?にすれば)以下でOK
    // val lv = spinner?.getModel()?.getValue() ?: 1000L
  }

手動変換

  • IntelliJの自動変換で正常に変換されるが、手動修正でもっと短いKotlinコードに変換が可能なケースのメモ

IntStream

  • java.util.stream.IntStreamIntRangeに置換可能
val indices = IntStream.range(0, l.getModel().getSize())
    .filter { rb.intersects(l.getCellBounds(it, it)) }.toArray()
val indices = (0 until l.getModel().getSize())
    .filter { rb.intersects(l.getCellBounds(it, it)) }.toIntArray()

List#get(0)

  • java.util.List#.get(0)List#firstOrNull()などに置換可能
var flag = 1
val keys = table.getRowSorter().getSortKeys()
if (!keys.isEmpty()) {
  val sortKey = keys.get(0)
  if (sortKey.getColumn() == column && sortKey.getSortOrder() == SortOrder.DESCENDING) {
    flag = -1
  }
}
val flag = table.getRowSorter().getSortKeys().firstOrNull()
    ?.takeIf { it.getColumn() == column && it.getSortOrder() == SortOrder.DESCENDING }
    ?.let { -1 } ?: 1

配列の最終要素

  • 配列の最終要素の取得は、Array#last()などに置換可能
p.setComponentZOrder(p.getComponent(p.getComponentCount() - 1), 0)
p.setComponentZOrder(p.getComponents().last(), 0)

Optional

  • Optional.ofNullable(...).filter(...)は、?.takeIf {...}などに置換可能
val clz = JTable::class.java
Optional.ofNullable(SwingUtilities.getAncestorOfClass(clz, e.getComponent()))
    .filter(clz::isInstance)
    .map(clz::cast)
    .filter(JTable::isEditing)
    .ifPresent(JTable::removeEditor)
SwingUtilities.getAncestorOfClass(JTable::class.java, e.getComponent())
    ?.takeIf { it is JTable }
    ?.let { it as JTable }
    ?.takeIf { it.isEditing() }
    ?.also{ it.removeEditor() }
// takeIf {...}内のitにはスマートキャストが効くが、それを越えて作用はしない
SwingUtilities.getAncestorOfClass(JTable::class.java, e.getComponent())
    ?.takeIf { it is JTable && it.isEditing() }
    ?.also { (it as JTable).removeEditor() }
    // ?.also { JTable::removeEditor } // コンパイルエラーにはならないが、removeEditor()メソッドは実行されない?

Objects.nonNull(...)

  • Objects.nonNull(o)などは、セーフコール演算子?.に置換可能
if (Objects.nonNull(colHead) && colHead.isVisible()) {
  val colHeadHeight = Math.min(availR.height, colHead.getPreferredSize().height)
  // ...
}
colHead?.takeIf { it.isVisible() }?.let {
  val colHeadHeight = Math.min(availR.height, it.getPreferredSize().height)
  // ...
}

Optional.orElse(...)

  • Optional.orElse(...)などは、エルビス演算子?:に置換可能
val lv = Optional.ofNullable(spinner).map { it.getModel().getValue() }.orElse(1000L)
val lv = spinner?.getModel()?.getValue() ?: 1000L

Stream

  • Streamは、Listに置換可能な場合が多い
fun stream(parent: Container): Stream<Component> = Arrays.stream(parent.getComponents())
    .filter(Container::class.java::isInstance)
    .map { c -> stream(Container::class.java.cast(c)) }
    .reduce(Stream.of<Component>(parent), { a, b -> Stream.concat<Component>(a, b) })
fun children(parent: Container): List<Component> = parent.getComponents()
    .filterIsInstance(Container::class.java)
    .map { children(it) }
    .fold(listOf<Component>(parent), { a, b -> a + b })

コメント