概要

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

実行環境

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

Gradle

buildscript {
  ext.kotlin_version = 'latest.release'
  repositories {
    mavenCentral()
  }
  dependencies {
    classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
  }
}

apply plugin: 'kotlin'
apply plugin: 'application'

// group 'KotlinSwingTips'
// version '1.0-SNAPSHOT'

mainClassName = 'example.AppKt'

defaultTasks 'run'

repositories {
  mavenCentral()
  jcenter()
}

dependencies {
  // implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
  implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
  testImplementation 'junit:junit:4.11'
  testImplementation "org.jetbrains.kotlin:kotlin-test-junit:$kotlin_version"
}

sourceCompatibility = 1.8

compileKotlin {
  kotlinOptions.jvmTarget = "$sourceCompatibility"
}

compileTestKotlin {
  kotlinOptions.jvmTarget = "$sourceCompatibility"
}

jar {
  manifest { attributes 'Main-Class': "$mainClassName" }
  from { configurations.compileClasspath.collect { it.isDirectory() ? it : zipTree(it) } }
}

IntelliJの自動変換

  • メニューのCode, Convert Java File to Kotlin File(Ctrl+Alt+Shift+K)で、JavaのソースコードをKotlinのソースコードに変換可能
    • Hoge.javaを変換するとHoge.ktが生成されて、Hoge.javaは削除されてしまうので注意が必要
      • Undo(Ctrl+Z)で変換は取り消されて削除されたJavaファイルは復活する
  • Fileメニュー、Project StructureパネルのProject SDK<No SDK>になっていると、@Overrideoverrideに変換されない場合がある
  • Ktolinプロジェクトを作成して適当なApp.ktを作成し、そこにJavaのソースコードをコピー・ペーストすると自動変換可能
    • Ctrl+Alt+Shift+Kでファイル変換するより高機能?

Improved Java to Kotlin converter

  • Kotlin 1.3.50 released | Kotlin Blog

    In the future, the new converter is going to become the default one. In this release, it’s available for preview. To turn it on, specify the Use New J2K (experimental) flag in settings.

  • Kotlin 1.3.50IntelliJ IDEAConvert Java File to Kotlin File(Ctrl+Alt+Shift+K)が実験的に改良されている
    • Use New J2K(experimental)フラグで試用可能になるが、設定をJ2Kで検索しても見つからない
    • Settingsダイアログ→Languages & FrameworksUse New Java to Kotlin Converterにチェックを入れる必要がある
    • Kotlin Plugin1.4.31-release-IJ2019.2-1から1.3.50-release-IJ2019.2-1にするとUse New Java to Kotlin Converterにチェックをしているかどうかに関わらず、KotlinNullPointerExceptionで変換に失敗する場合がある?
  • クリップボード経由の変換は1.3.50-release-IJ2019.2-1でもエラーにならず実行可能
  • 以下のほとんどがUse New J2K(experimental)をオンにすると修正されている

演算子の優先順位

  • 以下のようなソースコードを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の自動変換では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で自動変換すると失敗する場合がある
  • 逆に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の自動変換では正しく変換されるようになっている
  • インターフェイスから匿名内部クラスのインスタンスを生成する場合は()は不要
  • kotlin 1.4.0からkotlinのインターフェイスでもラムダでの記述も可能になった

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() {
  @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")
  }
})

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(手動修正1)

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

Kotlin(手動修正2)

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

Kotlin(手動修正3)

// runCatching関数でResultを取得するようtry-catchを置き換える
val bi = runCatching {
  ImageIO.read(javaClass.getResource("unkaku_w.png"))
}.getOrNull() ?: makeMissingImage()

メソッド参照

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

Java

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

Kotlin(自動変換)

fun descendants(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 descendants(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 descendants(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 descendants(parent: Container): List<Component> {
  return parent.getComponents().toList()
    .filterIsInstance(Container::class.java)
    .map { descendants(it) }
    .fold(listOf<Component>(parent)) { a, b -> a + b }
}
// ...
descendants(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)
}

Number#intValue()

  • Number#intValue()IntelliJで自動変換するとintValue()がそのまま使用されるが、コンパイルするとerror: unresolved reference: intValueとエラーになるので手動でtoInt()などを使用するよう修正する必要がある
    • 1.4.31-release-IJ2019.2-1の自動変換では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 2018.2.5の自動変換では、前処理でアンダースコアを除去?して正しく変換されるようになっている
    • 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)) // 問題なし

プライマリコンストラクタでのプロパティ定義

  • Javaのクラスで変数宣言にコメントが付いている場合、IntelliJの自動変換ではそのコメントがプライマリコンストラクタの引数でのプロパティ定義にそのままコピーされてしまう場合がある
    • たとえば以下の例では、 // = table.getTableHeader();class RowHeaderRenderer<E>(private val header: JTableHeader // = table.getTableHeader();) : JLabel(), ...とプライマリコンストラクタのプロパティ定義に入り込んでそれ以降がコメントアウトされてしまうため、コンパイルエラーになる
    • コメントの末尾で改行が入るよう修正された

Java

class RowHeaderRenderer<E> extends JLabel implements ListCellRenderer<E> {
  private final JTableHeader header; // = table.getTableHeader();

  public RowHeaderRenderer(JTableHeader header) {
    super();
    this.header = header;
    this.setOpaque(true);
    // ...

Kotlin(旧自動変換)

inner class RowHeaderRenderer<E>(private val header: JTableHeader // = table.getTableHeader();) : JLabel(), ListCellRenderer<E> {

  init {
    this.setOpaque(true)
    // ...

Kotlin(新自動変換)

inner class RowHeaderRenderer<E>(private val header: JTableHeader // = table.getTableHeader();
) : JLabel(), ListCellRenderer<E> {

  init {
    this.setOpaque(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の自動変換で二次元配列をクローンするコードを変換した場合、Type mismatch: inferred type is Array<Int?> but Array<Int> was expectedのようなエラーになる場合がある

Java

private final Integer[][] mask;
@SuppressWarnings("PMD.UseVarargs")
protected SudokuCellRenderer(Integer[][] src) {
  super();
  Integer[][] dest = new Integer[src.length][src[0].length];
  for (int i = 0; i < src.length; i++) {
    System.arraycopy(src[i], 0, dest[i], 0, src[0].length);
  }
  this.mask = dest;
}

Kotlin(自動変換)

private val mask: Array<Array<Int>>
init {
  val dest = Array<Array<Int>>(src.size) { arrayOfNulls(src[0].size) }
  for (i in src.indices) {
    System.arraycopy(src[i], 0, dest[i], 0, src[0].size)
  }
  this.mask = dest
}

Kotlin(手動修正1)

private val mask: Array<Array<Int?>>
init {
  val dest = Array<Array<Int?>>(src.size) { arrayOfNulls(src[0].size) }
  // もしくはval mask: Array<Array<Int>>のままで、適当なInt値で初期化
  // val dest = Array(src.size, { Array(src[0].size, { 0 }) }) // 0で初期化
  for (i in src.indices) {
    System.arraycopy(src[i], 0, dest[i], 0, src[0].size)
  }
  this.mask = dest
}

Kotlin(手動修正2)

private val mask: Array<Array<Int>>
init {
  // System.arraycopyなどは使用せず、コピー元の二次元配列を参照して初期化
  this.mask = Array(src.size, { i -> Array(src[i].size, { j -> src[i][j] }) })
}

Ellipse2D.Double

  • IntelliJの自動変換でEllipse2D.Doublekotlin.Double競合する場合がある?
    • 再現できなくなった

Java

import java.awt.geom.Ellipse2D;

enum ButtonLocation {
  CENTER(0d),
  NORTH(45d),
  EAST(135d),
  SOUTH(225d),
  WEST(-45d);
  private final double degree;
  ButtonLocation(double degree) {
    this.degree = degree;
  }

  public double getStartAngle() {
    return degree;
  }
}
// ...
Shape inner = new Ellipse2D.Double(xx, xx, ww, ww);

Kotlin(自動変換)

import java.awt.geom.Ellipse2D.Double

enum class ButtonLocation(val startAngle: kotlin.Double) {
  CENTER(0.0), NORTH(45.0), EAST(135.0), SOUTH(225.0), WEST(-45.0);
}
// ...
val inner: Shape = Double(xx, xx, ww, ww)

Kotlin(手動修正)

import java.awt.geom.Ellipse2D

enum class ButtonLocation(val startAngle: Double) {
  CENTER(0.0), NORTH(45.0), EAST(135.0), SOUTH(225.0), WEST(-45.0);
}

val inner: Shape = Ellipse2D.Double(xx, xx, ww, ww)

手動修正が不要な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
      // ...

enumを参照等価性で比較する

Java

checkBox.setSelected(node.getStatus() == Status.SELECTED);

Kotlin

checkBox.setSelected(it.status === Status.SELECTED)

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
  }
}

Null safety and platform types

  • 例えば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
  }

JComponent#createUI(...)

e: C:\kst\SearchBarLayoutComboBox\src\main\kotlin\example\BasicSearchBarComboBoxUI.kt: (189, 5): Accidental override: The following declarations have the same JVM signature (createUI(Ljavax/swing/JComponent;)Ljavax/swing/plaf/ComponentUI;):
    fun createUI(p0: JComponent!): ComponentUI! defined in javax.swing.plaf.basic.BasicComboBoxUI
    fun createUI(c: JComponent?): ComponentUI defined in example.BasicSearchBarComboBoxUI
  • 現状ではcreateUI(...)を使用せずupdateUI()をオーバーライドして直接独自UIを設定するなどで回避するしかない?

DefaultTreeCellEditor

  • JTree.startEditingAtPath(...)メソッドを実行すると以下のようなIllegalArgumentExceptionが発生する場合がある
    • このメソッドを辿るとBasicTreeUI内でイベントをnullTreeCellEditor#isCellEditable(event)を呼び出している箇所がある
    • その為、DefaultTreeCellEditor#isCellEditable(e: EventObject)のオーバーライドはDefaultTreeCellEditor#isCellEditable(e: EventObject?)に変更する必要がある
Exception in thread "AWT-EventQueue-0" java.lang.IllegalArgumentException: Parameter specified as non-null is null: method example.MainPanel$1.isCellEditable, parameter e
	at example.MainPanel$1.isCellEditable(App.kt)
	at javax.swing.plaf.basic.BasicTreeUI.startEditing(BasicTreeUI.java:2129)
	at javax.swing.plaf.basic.BasicTreeUI.startEditingAtPath(BasicTreeUI.java:620)
	at javax.swing.JTree.startEditingAtPath(JTree.java:2405)
	at example.TreePopupMenu$1.actionPerformed(App.kt:49)

Kotlin 自動変換

import java.awt.* // ktlint-disable no-wildcard-imports
import java.awt.event.MouseEvent
import java.util.EventObject
import javax.swing.* // ktlint-disable no-wildcard-imports
import javax.swing.event.AncestorEvent
import javax.swing.event.AncestorListener
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeCellEditor
import javax.swing.tree.DefaultTreeCellRenderer
import javax.swing.tree.TreePath

class MainPanel : JPanel(BorderLayout()) {
  init {
    val tree = JTree()
    tree.cellEditor = object : DefaultTreeCellEditor(tree, tree.cellRenderer as? DefaultTreeCellRenderer) {
      override fun isCellEditable(e: EventObject) = e !is MouseEvent && super.isCellEditable(e)
    }
    tree.isEditable = true
    tree.componentPopupMenu = TreePopupMenu()
    add(JScrollPane(tree))
    preferredSize = Dimension(320, 240)
  }
}

class TreePopupMenu : JPopupMenu() {
  private var path: TreePath? = null
  private val editItem: JMenuItem
  private val editDialogItem: JMenuItem

  override fun show(c: Component, x: Int, y: Int) {
    (c as? JTree)?.also { tree ->
      val tsp = tree.selectionPaths
      path = tree.getPathForLocation(x, y)
      val isEditable = tsp != null && tsp.size == 1 && tsp[0] == path
      editItem.isEnabled = isEditable
      editDialogItem.isEnabled = isEditable
      super.show(c, x, y)
    }
  }

  init {
    val field = JTextField()
    field.addAncestorListener(FocusAncestorListener())
    editItem = add("Edit")
    editItem.addActionListener {
      path?.also {
        (invoker as? JTree)?.startEditingAtPath(it)
      }
    }
    editDialogItem = add("Edit Dialog")
    editDialogItem.addActionListener {
      (path?.lastPathComponent as? DefaultMutableTreeNode)?.also { node ->
        field.text = node.userObject.toString()
        (invoker as? JTree)?.also { tree ->
          val ret = JOptionPane.showConfirmDialog(
            tree, field, "Rename", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE
          )
          if (ret == JOptionPane.OK_OPTION) {
            tree.model.valueForPathChanged(path, field.text)
          }
        }
      }
    }
    add("dummy")
  }
}

class FocusAncestorListener : AncestorListener {
  override fun ancestorAdded(e: AncestorEvent) {
    e.component.requestFocusInWindow()
  }

  override fun ancestorMoved(e: AncestorEvent) {
    /* not needed */
  }

  override fun ancestorRemoved(e: AncestorEvent) {
    /* not needed */
  }
}

fun main() {
  EventQueue.invokeLater {
    runCatching {
      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
    }.onFailure {
      it.printStackTrace()
      Toolkit.getDefaultToolkit().beep()
    }
    JFrame().apply {
      setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
      getContentPane().add(MainPanel())
      pack()
      setLocationRelativeTo(null)
      setVisible(true)
    }
  }
}

Kotlin 手動修正

import java.awt.* // ktlint-disable no-wildcard-imports
import java.awt.event.MouseEvent
import java.util.EventObject
import javax.swing.* // ktlint-disable no-wildcard-imports
import javax.swing.event.AncestorEvent
import javax.swing.event.AncestorListener
import javax.swing.tree.DefaultMutableTreeNode
import javax.swing.tree.DefaultTreeCellEditor
import javax.swing.tree.DefaultTreeCellRenderer
import javax.swing.tree.TreePath

class MainPanel : JPanel(BorderLayout()) {
  init {
    val tree = JTree()
    tree.cellEditor = object : DefaultTreeCellEditor(tree, tree.cellRenderer as? DefaultTreeCellRenderer) {
      override fun isCellEditable(e: EventObject) = e !is MouseEvent && super.isCellEditable(e)
    }
    tree.isEditable = true
    tree.componentPopupMenu = TreePopupMenu()
    add(JScrollPane(tree))
    preferredSize = Dimension(320, 240)
  }
}

class TreePopupMenu : JPopupMenu() {
  private var path: TreePath? = null
  private val editItem: JMenuItem
  private val editDialogItem: JMenuItem

  override fun show(c: Component, x: Int, y: Int) {
    (c as? JTree)?.also { tree ->
      val tsp = tree.selectionPaths
      path = tree.getPathForLocation(x, y)
      val isEditable = tsp != null && tsp.size == 1 && tsp[0] == path
      editItem.isEnabled = isEditable
      editDialogItem.isEnabled = isEditable
      super.show(c, x, y)
    }
  }

  init {
    val field = JTextField()
    field.addAncestorListener(FocusAncestorListener())
    editItem = add("Edit")
    editItem.addActionListener {
      path?.also {
        (invoker as? JTree)?.startEditingAtPath(it)
      }
    }
    editDialogItem = add("Edit Dialog")
    editDialogItem.addActionListener {
      (path?.lastPathComponent as? DefaultMutableTreeNode)?.also { node ->
        field.text = node.userObject.toString()
        (invoker as? JTree)?.also { tree ->
          val ret = JOptionPane.showConfirmDialog(
            tree, field, "Rename", JOptionPane.OK_CANCEL_OPTION, JOptionPane.PLAIN_MESSAGE
          )
          if (ret == JOptionPane.OK_OPTION) {
            tree.model.valueForPathChanged(path, field.text)
          }
        }
      }
    }
    add("JMenuItem")
  }
}

class FocusAncestorListener : AncestorListener {
  override fun ancestorAdded(e: AncestorEvent) {
    e.component.requestFocusInWindow()
  }

  override fun ancestorMoved(e: AncestorEvent) {
    /* not needed */
  }

  override fun ancestorRemoved(e: AncestorEvent) {
    /* not needed */
  }
}

fun main() {
  EventQueue.invokeLater {
    runCatching {
      UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
    }.onFailure {
      it.printStackTrace()
      Toolkit.getDefaultToolkit().beep()
    }
    JFrame().apply {
      setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE)
      getContentPane().add(MainPanel())
      pack()
      setLocationRelativeTo(null)
      setVisible(true)
    }
  }
}

手動変換

  • IntelliJの自動変換で正常に変換されるが、手動修正でもっと短いKotlinコードに変換が可能なケースのメモ
    • java.util.*のクラスを使用している箇所は変換可能な場合が多い

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()

Random

val lengthOfTask = 10 + Random().nextInt(50)
val lengthOfTask = (10..60).random()

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(...)

  • 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()メソッドは実行されない?

Optional#orElse(...)

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

Objects

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)
  // ...
}

Objects.requireNonNull(...)

  • Objects.requireNonNull(o)requireNotNullに置換可能
  • NonではなくNotになることに注意
Objects.requireNonNull(url)
requireNotNull(url)

Stream

  • Streamは、List(Iterable)に置換可能な場合が多い
  • 配列もIterableなのでStreamにする必要はあまりない
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 descendants(parent: Container): List<Component> = parent.getComponents()
    .filterIsInstance(Container::class.java)
    .map { descendants(it) }
    .fold(listOf<Component>(parent), { a, b -> a + b })

Stream.flatMap(...)

  • Stream#flatMap(Function<? super T,? extends Stream<? extends R>> mapper)はそのままIterable<T>.flatMap(transform: (T) -> Iterable<R>)に置換可能
// // Java
// private Stream<MenuElement> descendants(MenuElement me) {
//   return Stream.of(me.getSubElements())
//     .flatMap(m -> Stream.concat(Stream.of(m), descendants(m)));
// }
fun descendants(me: MenuElement): Stream<MenuElement> {
  return Stream.of(*me.subElements)
    .flatMap { Stream.concat(Stream.of(it), descendants(it)) }
}
// StreamではなくIterableが使用可能
private fun descendants(me: MenuElement): List<MenuElement> =
  me.getSubElements().flatMap { listOf(it) + descendants(it) }

Stream.reduce(...)

  • 初期値有りのStream#reduce(T identity, BinaryOperator<T> accumulator)は、Iterable<T>.fold(initial: R, operation: (acc: R, T) -> R)に置換可能
// // Java
// int max = group.stream()
//     .map(AlignedLabel::getSuperPreferredWidth)
//     .reduce(0, Integer::max);
val max = group.stream()
    .map(Function<AlignedLabel, Int> { it.getSuperPreferredWidth() })
    .reduce(0, BinaryOperator<Int> { a, b -> Integer.max(a, b) })
val max = group.stream()
    .map { it.getSuperPreferredWidth() }
    .reduce(0, Integer::max)
val max = group
    .map { it.getSuperPreferredWidth() }
    .fold(0) { a, b -> maxOf(a, b) }
val max = group.map { it.getSuperPreferredWidth() }.fold(0, ::maxOf)
// この例だとIntなのでmax()とエルビス演算子`?:`が使用可能
val max = group.map { it.getSuperPreferredWidth() }.max() ?: 0

Collections.nCopies(...).joinToString(...)

val text = Collections.nCopies(2000, "aaaaaaaaaaaaa").joinToString("\n")
val text = "aaaaaaaaaaaaa\n".repeat(2000)

joinToString(...)

String msg = jlist.getSelectedValuesList().stream()
    .map(it -> it.title)
    .collect(Collectors.joining(", "));
val msg = jlist.selectedValuesList
    .map { it.title }
    .joinToString(", ")

maxOf(...)

// private List<AlignedLabel> group;
int max = group.stream()
    .map(AlignedLabel::getSuperPreferredWidth)
    .reduce(0, Integer::max);
// private var group = mutableListOf<AlignedLabel>()
// val max = group.map { it.getSuperPreferredWidth() }.fold(0, ::maxOf)
// val max = group.map { it.getSuperPreferredWidth() }.maxOrNull() ?: 0
// group.isNotEmpty()
val max = group.maxOf(AlignedLabel::getSuperPreferredWidth)

複数行文字列

// private String makeTestHtml() {
//   return String.join("\n", strarray);
// }
// を自動変換すると以下のようなコードになる
private fun makeTestHtml(): String {
  return arrayOf(
    "<html><body>",
    "<div>2222222222</div>",
    "</body></html>").joinToString("\n")
}
// が、三重引用符で囲めば改行などをエスケープシーケンスで記入する必要はない
private fun makeTestHtml() = """
  <html>
    <body>
      <div>2222222222</div>
    </body>
  </html>
"""

Comparator.comparing(...).thenComparing(...)

  • 複数キーでソートするComparator<T>は、JavaではComparator.comparing(Function<? super T,? extends U> keyExtractor).thenComparing(Function<? super T,? extends U> keyExtractor)で作成可能
  • KotlinではcompareBy(vararg selectors: (T) -> Comparable<*>?)Comparator<T>.thenBy(...)で同等のComparator<T>が作成可能
// val tnc = Comparator.comparing(Function<DefaultMutableTreeNode, Boolean> { it.isLeaf() })
val tnc = Comparator.comparing<DefaultMutableTreeNode, Boolean> { it.isLeaf() }
    .thenComparing { n -> n.getUserObject().toString() }
val children = parent.children().toList()
    .filterIsInstance(DefaultMutableTreeNode::class.java)
    .sortedWith(tnc)
val tnc = compareBy<DefaultMutableTreeNode> { it.isLeaf() }
    .thenBy { it.getUserObject().toString() }
val children = parent.children().toList()
    .filterIsInstance(DefaultMutableTreeNode::class.java)
    .sortedWith(tnc)
    // .sortedWith(compareBy(DefaultMutableTreeNode::isLeaf, { it.getUserObject().toString() }))

filterIsInstance

// Only classes are allowed on the left hand side of a class literal
// とエラーになる
listOf(c0, c1, c2, c3)
  .mapNotNull { it.getModel() }
  .filterIsInstance(MutableComboBoxModel<String>::class.java)
  .forEach { it.insertElementAt(str, it.getSize()) }
// Type mismatch. でエラーになる
listOf(c0, c1, c2, c3)
  .mapNotNull { it.getModel() }
  .filterIsInstance(MutableComboBoxModel::class.java)
  .forEach { it.insertElementAt(str, it.getSize()) }
// reified type parameter を使用する
listOf(c0, c1, c2, c3)
  .mapNotNull { it.getModel() }
  .filterIsInstance<MutableComboBoxModel<String>>()
  .forEach { it.insertElementAt(str, it.getSize()) }
listOf(c0, c1, c2, c3)
  .mapNotNull { it.getModel() as? MutableComboBoxModel<String> }
  .forEach { it.insertElementAt(str, it.getSize()) }

Functional (SAM) interfaces

  • kotlin 1.4.0からKotlinインターフェイスでもSAM(Single Abstract Method)変換を利用可能になったが、まだIntelliJの自動変換ではFunctional interfacesではなく普通のinterfacesに変換されるのでこれを使用する場合は手動で置換する必要がある
interface ExpansionListener : EventListener {
   fun expansionStateChanged(e: ExpansionEvent)
 }
}
// ...
p.addExpansionListener(object : ExpansionListener {
  override fun expansionStateChanged(e: ExpansionEvent) {
    (e.source as? Component)?.also {
      // ...
    }
  }
})
fun interface ExpansionListener : EventListener {
   fun expansionStateChanged(e: ExpansionEvent)
 }
}
// ...
p.addExpansionListener { e ->
  (e.source as? Component)?.also {
    // ...
  }
}

Jetpack Compose for Desktop

コメント