概要

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

実行環境

$ curl -s "https://get.sdkman.io" | bash 
$ curl -s "https://get.sdkman.io" | bash
$ sdk install kotlin
$ kotlinc -version
info: kotlinc-jvm 1.1.4-3 (JRE 1.8.0_141-b15)
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

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

#spanend
#spanadd
apply plugin: 'kotlin'
#spanend
#spanadd
apply plugin: 'application'
#spanend
#spanadd

#spanend
#spanadd
// group 'KotlinSwingTips'
#spanend
#spanadd
// version '1.0-SNAPSHOT'
#spanend
#spanadd

#spanend
#spanadd
mainClassName = 'example.AppKt'
#spanend
#spanadd

#spanend
#spanadd
defaultTasks 'run'
#spanend
#spanadd

#spanend
#spanadd
repositories {
#spanend
  mavenCentral()
  jcenter()
#spanadd
}
#spanend
#spanadd

#spanend
#spanadd
dependencies {
#spanend
  // 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"
#spanadd
}
#spanend
#spanadd

#spanend
#spanadd
sourceCompatibility = 1.8
#spanend
#spanadd

#spanend
#spanadd
compileKotlin {
#spanend
  kotlinOptions.jvmTarget = "$sourceCompatibility"
#spanadd
}
#spanend
#spanadd

#spanend
#spanadd
compileTestKotlin {
#spanend
  kotlinOptions.jvmTarget = "$sourceCompatibility"
#spanadd
}
#spanend
#spanadd

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

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
#spanend
#spanadd
btn.setSelected((i & (1 << 2)) != 0);
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
btn.setSelected(i and (1 shl 2) != 0)
#spanend
#spanadd
// 以下のように誤変換される場合がある???
#spanend
#spanadd
// 再現しないので勘違いかもしれない
#spanend
#spanadd
// btn.setSelected(i and (1 shl 2 != 0))
#spanend
#spanadd

型パラメータ(修正済)

  • 修正済 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
#spanend
#spanadd
Box box = Box.createVerticalBox();
#spanend
#spanadd
for (Component c: Arrays.<Component>asList(textField1, textField2, combo3, combo4)) {
#spanend
  box.add(c);
  box.add(Box.createVerticalStrut(5));
#spanadd
}
#spanend
#spanadd
// for (Component c: Arrays.asList(...)) { でも同じ変換結果になる
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
val box = Box.createVerticalBox()
#spanend
#spanadd
for (c in Arrays.asList(textField1, textField2, combo3, combo4)) {
#spanend
  box.add(c)
  box.add(Box.createVerticalStrut(5))
#spanadd
}
#spanend
#spanadd
// error: none of the following functions can be called with the arguments supplied:
#spanend
#spanadd
// public open fun add(p0: Component!): Component! defined in javax.swing.Box
#spanend
#spanadd
// public open fun add(p0: PopupMenu!): Unit defined in javax.swing.Box
#spanend
#spanadd
//             box.add(c)
#spanend
#spanadd
//                 ^
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
val box = Box.createVerticalBox()
#spanend
#spanadd
for (c in Arrays.asList<Component>(textField1, textField2, combo3, combo4)) {
#spanend
  box.add(c);
  box.add(Box.createVerticalStrut(5));
#spanadd
}
#spanend
#spanadd
// or:
#spanend
#spanadd
// for (c in listOf<Component>(textField1, textField2, combo3, combo4)) {
#spanend
#spanadd
// for (c in Arrays<Component>.asList(textField1, textField2, combo3, combo4)) {
#spanend
#spanadd

境界型パラメータ

  • 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
#spanend
#spanadd
class CalendarViewTableModel<T extends LocalDate> extends DefaultTableModel {
#spanend
  public CalendarViewTableModel(T date) {
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
class CalendarViewTableModel<T : LocalDate> public constructor(date: T) : DefaultTableModel() {
#spanend
#spanadd

クラスリテラル(修正済)

  • 修正済 総称型を持つクラスのクラスリテラル(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
#spanend
#spanadd
enabledCheck.addActionListener(e -> {
#spanend
  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());
  }
#spanadd
});
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
enabledCheck.addActionListener({ e ->
#spanend
  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)
  }
#spanadd
})
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
enabledCheck.addActionListener {
#spanend
  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)
  }
#spanadd
}
#spanend
#spanadd

型引数の省略(修正済)

  • 修正済 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
#spanend
#spanadd
class CheckBoxList<E extends CheckBoxNode> extends JList<E> {
#spanend
  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);
  }
  // ...
#spanadd
}
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
internal class CheckBoxList<E : CheckBoxNode> protected constructor(model: ListModel<E>) : JList<E>(model) {
#spanend
  @Transient
  private var renderer: CheckBoxCellRenderer<E>? = null
#spanadd

#spanend
  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)
  }
  // ...
#spanadd
}
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
internal class CheckBoxList<E : CheckBoxNode> protected constructor(model: ListModel<E>) : JList<E>(model) {
#spanend
  @Transient
  private var renderer: CheckBoxCellRenderer<E>? = null
#spanadd

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

匿名内部クラス

  • IntelliJの自動変換ではerror: this class does not have a constructor tree.addTreeWillExpandListener(object : TreeWillExpandListener() {とエラーになる
    • IntelliJ 2018.2.5の自動変換では正しく変換されるようになっている
  • インターフェイスから匿名内部クラスのインスタンスを生成する場合は()は不要
  • kotlin 1.4.0からkotlinのインターフェイスでもラムダでの記述も可能になった
Java
#spanend
#spanadd
tree.addTreeWillExpandListener(new TreeWillExpandListener() {
#spanend
  @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");
  }
#spanadd
});
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
tree.addTreeWillExpandListener(object : TreeWillExpandListener() {
#spanend
  @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")
  }
#spanadd
})
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
tree.addTreeWillExpandListener(object : TreeWillExpandListener {
#spanend
  @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")
  }
#spanadd
})
#spanend
#spanadd

ラベル付きReturn

  • 匿名内部クラスからreturnで抜けるJavaコードをIntelliJの自動変換で変換すると、unresolved reference: @decodeとエラーになるKotlinコードに変換される場合がある
Java
#spanend
#spanadd
JButton decode = new JButton("decode");
#spanend
#spanadd
decode.addActionListener(e -> {
#spanend
  String b64 = textArea.getText();
  if (b64.isEmpty()) {
    return;
  }
  try (InputStream is = ...) {
    label.setIcon(new ImageIcon(ImageIO.read(is)));
  } catch (IOException ex) {
    ex.printStackTrace();
  }
#spanadd
});
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
val decode = JButton("decode")
#spanend
#spanadd
decode.addActionListener({ e ->
#spanend
  val b64 = textArea.getText()
  if (b64.isEmpty()) {
    return@decode.addActionListener
  }
  try {
    // ...
  } catch (ex: IOException) {
    ex.printStackTrace()
  }
#spanadd
})
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
val decode = JButton("decode")
#spanend
#spanadd
decode.addActionListener {
#spanend
  val b64 = textArea.getText()
  if (b64.isEmpty()) {
    return@addActionListener
  }
  try {
    // ...
  } catch (ex: IOException) {
    ex.printStackTrace()
  }
#spanadd
}
#spanend
#spanadd

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
#spanend
#spanadd
BufferedImage bi = Optional.ofNullable(getClass().getResource("unkaku_w.png"))
#spanend
    .map(url -> {
      try {
        return ImageIO.read(url);
      } catch (IOException ex) {
        return makeMissingImage();
      }
    }).orElseGet(() -> makeMissingImage());
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
val bi = Optional.ofNullable(javaClass.getResource("unkaku_w.png")).map({ url ->
#spanend
  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 ()
  }
#spanadd
}).orElseGet({ makeMissingImage() })
#spanend
#spanadd
Kotlin(手動修正1)
#spanend
#spanadd
val bi = Optional.ofNullable(javaClass.getResource("unkaku_w.png")).map { url ->
#spanend
  try {
    ImageIO.read(url)
  } catch (ex: IOException) {
    makeMissingImage()
  }
#spanadd
}.orElseGet { makeMissingImage() }
#spanend
#spanadd
Kotlin(手動修正2)
#spanend
#spanadd
val bi = javaClass.getResource("unkaku_w.png")?.let {
#spanend
  try {
    ImageIO.read(it)
  } catch (ex: IOException) {
    makeMissingImage()
  }
#spanadd
} ?: makeMissingImage()
#spanend
#spanadd
Kotlin(手動修正3)
#spanend
#spanadd
// runCatching関数でResultを取得するようtry-catchを置き換える
#spanend
#spanadd
val bi = runCatching {
#spanend
  ImageIO.read(javaClass.getResource("unkaku_w.png"))
#spanadd
}.getOrNull() ?: makeMissingImage()
#spanend
#spanadd

メソッド参照

  • IntelliJの自動変換ではメソッド参照の変換が苦手? いくらか修正されている?
    • IntelliJ 2018.2.5の自動変換では、???ではなくPredicate<Component>などに変換されるようになった
Java
#spanend
#spanadd
public static Stream<TreeNode> descendants(TreeNode node) {
#spanend
  Class<TreeNode> clz = TreeNode.class;
  return Collections.list((Enumeration<?>) node.children())
    .stream().filter(clz::isInstance).map(clz::cast);
#spanadd
}
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
fun descendants(node: TreeNode): Stream<TreeNode> {
#spanend
  val clz = TreeNode::class.java
  return Collections.list(node.children() as Enumeration<*>)
    .stream().filter(???({ clz!!.isInstance() })).map(clz!!.cast)
#spanadd
}
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
fun descendants(node: TreeNode): Stream<TreeNode> {
#spanend
  val clz = TreeNode::class.java
  return Collections.list(node.children() as Enumeration<*>)
    .stream().filter(clz::isInstance).map(clz::cast)
    // .filterIsInstance(clz).stream()
#spanadd
}
#spanend
#spanadd
Kotlin(手動修正): Iterable<*>.filterIsInstance()が便利
#spanend
#spanadd
fun descendants(node: TreeNode): List<TreeNode> {
#spanend
  return (node.children() as Enumeration<*>)
    .toList().filterIsInstance(TreeNode::class.java)
#spanadd
}
#spanend
#spanadd
  • 例えば.filter(Container.class::isInstance)のようなメソッド参照を、IntelliJ 2018.2.5の自動変換では.filter(Predicate<Component> { Container::class.java!!.isInstance(it) })のように変換可能になった?が、importの追加は対応していない
Java
#spanend
#spanadd
public static Stream<Component> stream(Container parent) {
#spanend
  return Stream.of(parent.getComponents())
    .filter(Container.class::isInstance)
    .map(c -> stream(Container.class.cast(c)))
    .reduce(Stream.of(parent), Stream::concat);
  }
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
// 自動的に以下のimportは生成されないのでunresolved referenceになる
#spanend
#spanadd
// import java.util.function.BinaryOperator
#spanend
#spanadd
// import java.util.function.Predicate
#spanend
#spanadd
fun stream(parent: Container): Stream<Component> {
#spanend
  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) })
#spanadd
}
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
import java.util.function.BinaryOperator
#spanend
#spanadd
import java.util.function.Predicate
#spanend
#spanadd
fun stream(parent: Container): Stream<Component> {
#spanend
  // 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
#spanadd
}
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
// Stream を Iterable に変換してしまう方法もある
#spanend
#spanadd
fun descendants(parent: Container): List<Component> {
#spanend
  return parent.getComponents().toList()
    .filterIsInstance(Container::class.java)
    .map { descendants(it) }
    .fold(listOf<Component>(parent)) { a, b -> a + b }
#spanadd
}
#spanend
#spanadd
// ...
#spanend
#spanadd
descendants(fileChooser1)
#spanend
  .filterIsInstance(JTable::class.java)
  // .firstOrNull()?.apply(JTable::removeEditor)
  .firstOrNull()?.let { table ->
    println(table)
    table.removeEditor()
  }
#spanadd

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

  • 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
#spanend
#spanadd
JPanel p = new JPanel(new GridLayout(0, 1, 2, 2));
#spanend
#spanadd
Arrays.asList(button1, button2).forEach(p::add);
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
val p = JPanel(GridLayout(0, 1, 2, 2))
#spanend
#spanadd
Arrays.asList(button1, button2).forEach( ?? ? ({ p.add() }))
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
val p = JPanel(GridLayout(0, 1, 2, 2))
#spanend
#spanadd
listOf(button1, button2).forEach { b -> p.add(b) }
#spanend
#spanadd
//or listOf(button1, button2).map(p::add)
#spanend
#spanadd

コンストラクタ参照

  • 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
#spanend
#spanadd
Stream.of("A", "B", "C").map(JToggleButton::new).forEach(r -> {
#spanend
  p.add(r);
  bg.add(r);
#spanadd
});
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
Stream.of("A", "B", "C").map(Function<String, JToggleButton> { JToggleButton(it) }).forEach({ r ->
#spanend
  p.add(r)
  bg.add(r)
#spanadd
})
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
Stream.of("A", "B", "C").map(::JToggleButton).forEach { r ->
#spanend
  p.add(r)
  bg.add(r)
#spanadd
}
#spanend
#spanadd

Number#intValue()

  • Number#intValue()IntelliJで自動変換するとintValue()がそのまま使用されるが、コンパイルするとerror: unresolved reference: intValueとエラーになるので手動でtoInt()などを使用するよう修正する必要がある
    • 1.4.31-release-IJ2019.2-1の自動変換ではtoInt()になる
Java
#spanend
#spanadd
double d = delta.y * GRAVITY;
#spanend
#spanadd
int ia = (int) d;
#spanend
#spanadd
int ib = (int) Math.floor(d);
#spanend
#spanadd
int ic = new BigDecimal(d).setScale(0, RoundingMode.DOWN).intValue();
#spanend
#spanadd
System.out.format("%d %d %d%n", ia, ib, ic);
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
val d = delta.y * GRAVITY
#spanend
#spanadd
val ia = d.toInt()
#spanend
#spanadd
val ib = Math.floor(d) as Int
#spanend
#spanadd
val ic = BigDecimal(d).setScale(0, RoundingMode.DOWN).intValue()
#spanend
#spanadd
System.out.format("%d %d %d%n", ia, ib, ic)
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
val d = delta.y * GRAVITY
#spanend
#spanadd
val ia = d.toInt()
#spanend
#spanadd
val ib = Math.floor(d).toInt()
#spanend
#spanadd
val ic = BigDecimal(d).setScale(0, RoundingMode.DOWN).toInt()
#spanend
#spanadd
println("${ia} ${ib} ${ic}")
#spanend
#spanadd

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
#spanend
#spanadd
import java.util.*;
#spanend
#spanadd
public class SortTest {
#spanend
  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);
  }
#spanadd
}
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
fun main(args: Array<String>) {
#spanend
  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)
#spanadd
}
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
fun main(args: Array<String>) {
#spanend
  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)
#spanadd
}
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
fun main(args: Array<String>) {
#spanend
  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)
#spanadd
}
#spanend
#spanadd

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
#spanend
#spanadd
editor1.setSelectionColor(new Color(0x64_88_AA_AA, true)); // IntelliJで自動変換できない
#spanend
#spanadd
editor2.setSelectionColor(new Color(0x6488AAAA, true)); // IntelliJで自動変換可能
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
editor1.setSelectionColor(Color(0x64_88_AA_AA, true)) // 問題なし
#spanend
#spanadd
editor2.setSelectionColor(Color(0x6488AAAA, true)) // 問題なし
#spanend
#spanadd

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

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

#spanend
  public RowHeaderRenderer(JTableHeader header) {
    super();
    this.header = header;
    this.setOpaque(true);
    // ...
#spanadd
Kotlin(旧自動変換)
#spanend
#spanadd
inner class RowHeaderRenderer<E>(private val header: JTableHeader // = table.getTableHeader();) : JLabel(), ListCellRenderer<E> {
#spanend
#spanadd

#spanend
  init {
    this.setOpaque(true)
    // ...
#spanadd
Kotlin(新自動変換)
#spanend
#spanadd
inner class RowHeaderRenderer<E>(private val header: JTableHeader // = table.getTableHeader();
#spanend
#spanadd
) : JLabel(), ListCellRenderer<E> {
#spanend
#spanadd

#spanend
  init {
    this.setOpaque(true)
    // ...
#spanadd

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
#spanend
#spanadd
Color argb = new Color(0xffaabbcc, true);
#spanend
#spanadd
Color rgb = new Color(0xaabbcc);
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
val argb = Color(-0x554434, true)
#spanend
#spanadd
val rgb = Color(0xaabbcc)
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
// LongからIntに変換する方法もある
#spanend
#spanadd
val argb = Color(0xff_aa_bb_cc.toInt(), true)
#spanend
#spanadd
println(argb == Color(-0x554434, true)) // true
#spanend
#spanadd

@SuppressWarnings

  • IntelliJの自動変換では、@SuppressWarnings("unchecked")などは削除されるので、Unchecked cast: WatchEvent<*>! to WatchEvent<Path>と警告される
    • 手動で@Suppress("UNCHECKED_CAST")などを付ける必要がある
Java
#spanend
#spanadd
@SuppressWarnings("unchecked")
#spanend
#spanadd
WatchEvent<Path> ev = (WatchEvent<Path>) event;
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
@Suppress("UNCHECKED_CAST")
#spanend
#spanadd
val ev = event as WatchEvent<Path>
#spanend
#spanadd

二次元配列

  • IntelliJの自動変換で二次元配列をクローンするコードを変換した場合、Type mismatch: inferred type is Array<Int?> but Array<Int> was expectedのようなエラーになる場合がある
Java
#spanend
#spanadd
private final Integer[][] mask;
#spanend
#spanadd
@SuppressWarnings("PMD.UseVarargs")
#spanend
#spanadd
protected SudokuCellRenderer(Integer[][] src) {
#spanend
  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;
#spanadd
}
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
private val mask: Array<Array<Int>>
#spanend
#spanadd
init {
#spanend
  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
#spanadd
}
#spanend
#spanadd
Kotlin(手動修正1)
#spanend
#spanadd
private val mask: Array<Array<Int?>>
#spanend
#spanadd
init {
#spanend
  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
#spanadd
}
#spanend
#spanadd
Kotlin(手動修正2)
#spanend
#spanadd
private val mask: Array<Array<Int>>
#spanend
#spanadd
init {
#spanend
  // System.arraycopyなどは使用せず、コピー元の二次元配列を参照して初期化
  this.mask = Array(src.size, { i -> Array(src[i].size, { j -> src[i][j] }) })
#spanadd
}
#spanend
#spanadd

Ellipse2D.Double

  • IntelliJの自動変換でEllipse2D.Doublekotlin.Double競合する場合がある?
    • 再現できなくなった
Java
#spanend
#spanadd
import java.awt.geom.Ellipse2D;
#spanend
#spanadd

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

#spanend
  public double getStartAngle() {
    return degree;
  }
#spanadd
}
#spanend
#spanadd
// ...
#spanend
#spanadd
Shape inner = new Ellipse2D.Double(xx, xx, ww, ww);
#spanend
#spanadd
Kotlin(自動変換)
#spanend
#spanadd
import java.awt.geom.Ellipse2D.Double
#spanend
#spanadd

#spanend
#spanadd
enum class ButtonLocation(val startAngle: kotlin.Double) {
#spanend
  CENTER(0.0), NORTH(45.0), EAST(135.0), SOUTH(225.0), WEST(-45.0);
#spanadd
}
#spanend
#spanadd
// ...
#spanend
#spanadd
val inner: Shape = Double(xx, xx, ww, ww)
#spanend
#spanadd
Kotlin(手動修正)
#spanend
#spanadd
import java.awt.geom.Ellipse2D
#spanend
#spanadd

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

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

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

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

セーフキャスト

Java
#spanend
#spanadd
Set<?> f = v instanceof Set ? (Set<?>) v : EnumSet.noneOf(Permissions.class);
#spanend
#spanadd
Kotlin
#spanend
#spanadd
val f = v as? Set<*> ?: EnumSet.noneOf(Permissions::class.java)
#spanend
#spanadd

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

Java
#spanend
#spanadd
class FileListTable extends JTable {
#spanend
  private class RubberBandingListener extends MouseAdapter {
    @Override public void mousePressed(MouseEvent e) {
      FileListTable table = FileListTable.this
      // ...
#spanadd
Kotlin
#spanend
#spanadd
class FileListTable(model: TableModel) : JTable(model) {
#spanend
  private inner class RubberBandingListener : MouseAdapter() {
    override fun mousePressed(e: MouseEvent) {
      val table = this@FileListTable
      // ...
#spanadd

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

  • 最近?のIntelliJの自動変換では、列挙子(enum)の比較を===演算子に変換する
Java
#spanend
#spanadd
checkBox.setSelected(node.getStatus() == Status.SELECTED);
#spanend
#spanadd
Kotlin
#spanend
#spanadd
checkBox.setSelected(it.status === Status.SELECTED)
#spanend
#spanadd

Swing + Kotlin サンプル

SwingUtilities.getAncestorOfClass(...)

Java
#spanend
#spanadd
Container p = SwingUtilities.getAncestorOfClass(JViewport.class, this);
#spanend
#spanadd
if (!(p instanceof JViewport)) {
#spanend
  return;
#spanadd
}
#spanend
#spanadd
Kotlin
#spanend
#spanadd
val p = SwingUtilities.getAncestorOfClass(JViewport::class.java, this) as? JViewport ?: return
#spanend
#spanadd

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

インターフェイス、匿名内部クラス、throws

  • addTreeWillExpandListener(object: TreeWillExpandListener() { ...と記述するとerror: this class does not have a constructorとエラーになる
  • インターフェイスから匿名内部クラスのインスタンスを生成する場合は()は不要で、addTreeWillExpandListener(object: TreeWillExpandListener { ...と記述する
Java
#spanend
#spandel
import java.awt.*;
#spanend
#spandel
import javax.swing.*;
#spanend
#spandel
import javax.swing.event.*;
#spanend
#spandel
import javax.swing.plaf.IconUIResource;
#spanend
#spandel
import javax.swing.tree.*;
#spanend
#spandel

#spanend
#spandel
public class ThrowsExample {
#spanend
  public JComponent makeUI() {
    JPanel p = new JPanel(new GridLayout(1, 0, 5, 5));
    p.add(new JScrollPane(new JTree()));
    p.add(new JScrollPane(makeTree()));
    p.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5));
    return p;
  }
  private static JTree makeTree() {
    Icon icon = new EmptyIcon();
    UIManager.put("Tree.expandedIcon",  new IconUIResource(icon));
    UIManager.put("Tree.collapsedIcon", new IconUIResource(icon));
#spandel

#spanend
    JTree tree = new JTree();
    tree.setEditable(true);
    tree.setBorder(BorderFactory.createEmptyBorder(4, 4, 4, 4));
    int row = 0;
    while (row < tree.getRowCount()) {
      tree.expandRow(row++);
    }
    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");
      }
    });
    return tree;
  }
  public static void main(String[] args) {
    EventQueue.invokeLater(() -> {
      JFrame f = new JFrame();
      f.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
      f.getContentPane().add(new ThrowsExample().makeUI());
      f.setSize(320, 240);
      f.setLocationRelativeTo(null);
      f.setVisible(true);
    });
  }
#spandel
}
#spanend
#spandel

#spanend
#spandel
class EmptyIcon implements Icon {
#spanend
  @Override public void paintIcon(Component c, Graphics g, int x, int y) {
    /* Empty icon */
  }
  @Override public int getIconWidth() {
    return 0;
  }
  @Override public int getIconHeight() {
    return 0;
  }
#spandel
}
#spanend
#spandel
Kotlin
#spanend
#spandel
import java.awt.*
#spanend
#spandel
import javax.swing.*
#spanend
#spandel
import javax.swing.event.*
#spanend
#spandel
import javax.swing.plaf.IconUIResource
#spanend
#spandel
import javax.swing.tree.*
#spanend
#spandel

#spanend
#spandel
fun makeUI(): JComponent {
#spanend
  val emptyIcon = EmptyIcon()
  UIManager.put("Tree.expandedIcon",  IconUIResource(emptyIcon))
  UIManager.put("Tree.collapsedIcon", IconUIResource(emptyIcon))
#spandel

#spanend
  val tree = JTree().apply {
    setEditable(true)
    border = BorderFactory.createEmptyBorder(4, 4, 4, 4)
    addTreeWillExpandListener(object: TreeWillExpandListener {
      @Throws(ExpandVetoException::class)
      override fun treeWillExpand(e: TreeExpansionEvent) {
        //throw ExpandVetoException(e, "Tree expansion cancelled")
      }
      @Throws(ExpandVetoException::class)
      override fun treeWillCollapse(e: TreeExpansionEvent) {
        throw ExpandVetoException(e, "Tree collapse cancelled")
      }
    })
  }
#spandel

#spanend
  var row = 0;
  while (row < tree.getRowCount()) {
    tree.expandRow(row++);
  }
#spandel

#spanend
  return JPanel(GridLayout(1, 0)).apply {
    setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5))
    add(JScrollPane(JTree()))
    add(JScrollPane(tree))
  }
#spandel
}
#spanend
#spandel

#spanend
#spandel
class EmptyIcon: Icon {
#spanend
    override fun paintIcon(c: Component, g: Graphics , x: Int, y: Int) {
      /* Empty icon */
    }
    override fun getIconWidth():  Int = 0
    override fun getIconHeight(): Int = 0
#spandel
}
#spanend
#spandel

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

Optional

  • Optional#orElse(...)は、let {...} ?: ...に変換
    • Integer stepSize = Optional.ofNullable(stepSizeMap.get(calendarField)).orElse(1);
    • val stepSize = stepSizeMap.get(calendarField).let { it } ?: 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
#spanend
#spandel
import java.awt.*;
#spanend
#spandel
import java.text.*;
#spanend
#spandel
import java.util.*;
#spanend
#spandel
import javax.swing.*;
#spanend
#spandel
import javax.swing.text.*;
#spanend
#spandel

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

#spanend
    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();
#spandel

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

#spanend
    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);
#spandel

#spanend
    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);
#spandel

#spanend
    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);
    });
  }
#spandel
}
#spanend
#spandel
Kotlin
#spanend
#spandel
import java.awt.*
#spanend
#spandel
import java.text.*
#spanend
#spandel
import java.util.*
#spanend
#spandel
import javax.swing.*
#spanend
#spandel
import javax.swing.text.*
#spanend
#spandel

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

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

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

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

#spanend
  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)
  }
#spandel

#spanend
  return JPanel().apply {
    add(sp1)
    add(sp2)
  }
#spandel
}
#spanend
#spandel

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

演算子の優先順位

  • -

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")
      }
      #spandel
      //...
      #spanend
      #spanadd
      // ...
      #spanend
      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
  }
}

Arrays.asList(...)

  • 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(...)) {を使用すると、hello.kt:97:26: error: expecting an elementとエラーになる
Java
#spanend
#spandel
Box box = Box.createVerticalBox();
#spanend
#spandel
for (Component c: Arrays.<Component>asList(textField1, textField2, combo3, combo4)) {
#spanend
  box.add(c);
  box.add(Box.createVerticalStrut(5));
#spandel
}
#spanend
#spandel
// for (Component c: Arrays.asList(...)) { でも同じ変換結果になる
#spanend
#spandel
Kotlin(自動変換)
#spanend
#spandel
val box = Box.createVerticalBox()
#spanend
#spandel
for (c in Arrays.asList(textField1, textField2, combo3, combo4)) {
#spanend
  box.add(c)
  box.add(Box.createVerticalStrut(5))
#spandel
}
#spanend
#spandel
// hello.kt:100:17: error: none of the following functions can be called with the arguments supplied:
#spanend
#spandel
// public open fun add(p0: Component!): Component! defined in javax.swing.Box
#spanend
#spandel
// public open fun add(p0: PopupMenu!): Unit defined in javax.swing.Box
#spanend
#spandel
//             box.add(c)
#spanend
#spandel
//                 ^
#spanend
#spandel
Kotlin(手動修正)
#spanend
#spandel
val box = Box.createVerticalBox()
#spanend
#spandel
for (c in Arrays.asList<Component>(textField1, textField2, combo3, combo4)) {
#spanend
  box.add(c);
  box.add(Box.createVerticalStrut(5));
#spandel
}
#spanend
#spandel
// or:
#spanend
#spandel
// for (c in listOf<Component>(textField1, textField2, combo3, combo4)) {
#spanend
#spandel
// for (c in Arrays<Component>.asList(textField1, textField2, combo3, combo4)) {
#spanend
#spandel

JTree, ActionListener

ActionListener

  • IntelliJの自動変換ではActionListenerなどの関数型インターフェースのラムダ式をうまく変換できない場合がある
    • AbstractCellEditor#stopCellEditing()booleanを返すのでerror: type mismatch: inferred type is (???) -> Boolean but ActionListener? was expectedとエラーになる
    • 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;
#spandel
//...
#spanend
#spanadd
// ...
#spanend
handler = e -> stopCellEditing();

Kotlin(自動変換)

protected var handler: ActionListener? = null
#spandel
//...
#spanend
#spanadd
// ...
#spanend
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
#spandel
//...
#spanend
#spanadd
// ...
#spanend
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;
    }
#spandel

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

メソッド参照

  • IntelliJの自動変換ではメソッド参照の変換が苦手?

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

#spandel
public static Stream<TreeNode> children(TreeNode node) {
#spanend
  Class<TreeNode> clz = TreeNode.class;
  return Collections.list((Enumeration<?>) node.children())
    .stream().filter(clz::isInstance).map(clz::cast);
#spanadd
import java.awt.*;
#spanend
#spanadd
import java.text.*;
#spanend
#spanadd
import java.util.*;
#spanend
#spanadd
import javax.swing.*;
#spanend
#spanadd
import javax.swing.text.*;
#spanend
#spanadd

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

#spanend
    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();
#spanadd

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

#spanend
    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);
#spanadd

#spanend
    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);
#spanadd

#spanend
    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(自動変換) Kotlin
#spandel
fun children(node: TreeNode): Stream<TreeNode> {
#spanend
  val clz = TreeNode::class.java
  return Collections.list(node.children() as Enumeration<*>)
    .stream().filter(???({ clz!!.isInstance() })).map(clz!!.cast)
#spanadd
import java.awt.*
#spanend
#spanadd
import java.text.*
#spanend
#spanadd
import java.util.*
#spanend
#spanadd
import javax.swing.*
#spanend
#spanadd
import javax.swing.text.*
#spanend
#spanadd

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

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

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

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

#spanend
  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)
  }
#spanadd

#spanend
  return JPanel().apply {
    add(sp1)
    add(sp2)
  }
}
#spandel
Kotlin(手動修正)
#spanend
#spandel
fun children(node: TreeNode): Stream<TreeNode> {
#spanend
  val clz = TreeNode::class.java
  return Collections.list(node.children() as Enumeration<*>)
    .stream().filter(clz::isInstance).map(clz::cast)
#spanadd
fun main(args: Array<String>) {
#spanend
  EventQueue.invokeLater {
    JFrame("kotlin swing").apply {
      defaultCloseOperation = WindowConstants.EXIT_ON_CLOSE
      add(makeUI())
      size = Dimension(320, 240)
      setLocationRelativeTo(null)
      setVisible(true)
    }
  }
}
  • Container#add(Component)メソッドは戻り値としてComponentを返す
    • Javaの場合、終端処理のforEach(p::add)では戻り値を無視してContainer#add(Component)をメソッド参照することが可能
    • Kotlinの場合、forEach(p::add)とすると、addメソッドで引数がComponent!、戻り値がUnitになるメソッドを探す?ためメソッド参照で以下のようなエラーになる

SwingWorker

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

JComponent#updateUI()

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

Java

#spandel
JPanel p = new JPanel(new GridLayout(0, 1, 2, 2));
#spanend
#spandel
Arrays.asList(button1, button2).forEach(p::add);
#spanend
#spanadd
public final class MainPanel extends JPanel {
#spanend
  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
#spanend
#spanadd
class MainPanel : JPanel(BorderLayout()) {
#spanend
  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
#spanadd

#spanend
    // NullPointerException
    // val lv = spinner.getModel().getValue()
#spanadd

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

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

JComponent#createUI(...)

#spanend
#spanadd
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;):
#spanend
    fun createUI(p0: JComponent!): ComponentUI! defined in javax.swing.plaf.basic.BasicComboBoxUI
    fun createUI(c: JComponent?): ComponentUI defined in example.BasicSearchBarComboBoxUI
#spanadd
  • 現状ではcreateUI(...)を使用せずupdateUI()をオーバーライドして直接独自UIを設定するなどで回避するしかない?

DefaultTreeCellEditor

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

#spanend
#spanadd
class MainPanel : JPanel(BorderLayout()) {
#spanend
  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)
  }
#spanadd
}
#spanend
#spanadd

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

#spanend
  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)
    }
  }
#spanadd

#spanend
  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")
  }
#spanadd
}
#spanend
#spanadd

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

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

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

#spanend
#spanadd
fun main() {
#spanend
  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)
    }
  }
#spanadd
}
#spanend
#spanadd
Kotlin 手動修正
#spandel
val p = JPanel(GridLayout(0, 1, 2, 2))
#spanend
#spandel
Arrays.asList(button1, button2).forEach( ?? ? ({ p.add() }))
#spanend
#spanadd
import java.awt.* // ktlint-disable no-wildcard-imports
#spanend
#spanadd
import java.awt.event.MouseEvent
#spanend
#spanadd
import java.util.EventObject
#spanend
#spanadd
import javax.swing.* // ktlint-disable no-wildcard-imports
#spanend
#spanadd
import javax.swing.event.AncestorEvent
#spanend
#spanadd
import javax.swing.event.AncestorListener
#spanend
#spanadd
import javax.swing.tree.DefaultMutableTreeNode
#spanend
#spanadd
import javax.swing.tree.DefaultTreeCellEditor
#spanend
#spanadd
import javax.swing.tree.DefaultTreeCellRenderer
#spanend
#spanadd
import javax.swing.tree.TreePath
#spanend
#spanadd

#spanend
#spanadd
class MainPanel : JPanel(BorderLayout()) {
#spanend
  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)
  }
#spanadd
}
#spanend
#spanadd

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

#spanend
  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)
    }
  }
#spanadd

#spanend
  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")
  }
#spanadd
}
#spanend
#spanadd

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

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

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

#spanend
#spanadd
fun main() {
#spanend
  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)
    }
  }
#spanadd
}
#spanend
Kotlin(手動修正)

手動変換

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

IntStream

  • java.util.stream.IntStreamIntRangeに置換可能
#spandel
val p = JPanel(GridLayout(0, 1, 2, 2))
#spanend
#spandel
listOf(button1, button2).forEach({ b -> p.add(b) })
#spanend
#spandel
//or listOf(button1, button2).map(p::add)
#spanend
#spanadd
val indices = IntStream.range(0, l.getModel().getSize())
#spanend
    .filter { rb.intersects(l.getCellBounds(it, it)) }.toArray()
#spanend
#spanadd
val indices = (0 until l.getModel().getSize())
#spanend
    .filter { rb.intersects(l.getCellBounds(it, it)) }.toIntArray()
#spanadd

Random

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

List#get(0)

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

配列の最終要素

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

Optional

Optional.ofNullable(...)

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

Optional#orElse(...)

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

Objects

Objects.nonNull(...)

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

Objects.requireNonNull(...)

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

Stream

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

Stream.flatMap(...)

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

Stream.reduce(...)

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

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

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

joinToString(...)

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

maxOf(...)

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

複数行文字列

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

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>が作成可能
#spanend
#spanadd
// val tnc = Comparator.comparing(Function<DefaultMutableTreeNode, Boolean> { it.isLeaf() })
#spanend
#spanadd
val tnc = Comparator.comparing<DefaultMutableTreeNode, Boolean> { it.isLeaf() }
#spanend
    .thenComparing { n -> n.getUserObject().toString() }
#spanadd
val children = parent.children().toList()
#spanend
    .filterIsInstance(DefaultMutableTreeNode::class.java)
    .sortedWith(tnc)
#spanadd
#spanend
#spanadd
val tnc = compareBy<DefaultMutableTreeNode> { it.isLeaf() }
#spanend
    .thenBy { it.getUserObject().toString() }
#spanadd
val children = parent.children().toList()
#spanend
    .filterIsInstance(DefaultMutableTreeNode::class.java)
    .sortedWith(tnc)
    // .sortedWith(compareBy(DefaultMutableTreeNode::isLeaf, { it.getUserObject().toString() }))
#spanadd

filterIsInstance

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

Functional (SAM) interfaces

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

Jetpack Compose for Desktop

コメント