who’s watching

by orekyuu 0 Comments

はじめに

Java Puzzlers Advent Calendar 2016最終日の記事です。
皆さんはこれまでの問題は解けたでしょうか?難しい問題ばかりで僕の正答率はガタガタでしたw

というわけで今日が最後の問題になります。1ヶ月ほど前に記事にしていたのですが、結構面白い内容だったので今回のカレンダー用に再出題してみます。

問題

次のコードの出力は何になるでしょうか?

public class Main {

    public static void main(String[] args) {
        String hello = "hello";

        Runnable a = () -> System.out.println("hello");
        Runnable b = () -> System.out.println(hello);
        Runnable c = new Runnable() {
            @Override
            public void run() {
                System.out.println("hello");
            }
        };

        WeakReference<Runnable> ref1 = new WeakReference<>(a);
        WeakReference<Runnable> ref2 = new WeakReference<>(b);
        WeakReference<Runnable> ref3 = new WeakReference<>(c);

        a = null;
        b = null;
        c = null;

        System.gc();

        System.out.println(ref1.get() == null);
        System.out.println(ref2.get() == null);
        System.out.println(ref3.get() == null);
    }
}

WeakReferenceを知らない人もいるかもしれないので簡単に解説します。
私達が変数などで使っているのは強参照といいます。WeakReferenceはコンストラクタに与えられたインスタンスが強参照またはソフト参照(今回は使っていない)されなくなった時にGC対象になり、getした時にnullを返すようになります。

それを踏まえた上で今回の問題を考えてみてください!

解説と答え

まず一番わかりやすいref3からみてみましょう。
これは当然GC対象になりnullになります。

次にref1を見てみます。
これの出力はなんとfalseになります。つまりコードにはないどこかからか見ている物があるということです。
ラムダ式はinvokedynamic命令で実行される(必ず使われるというわけではないらしい)のですが、これを使って動的にクラスを作る時作ったインスタンスをキャッシュする仕組みがあります。毎回同じ内容のインスタンスを作るならキャッシュして使い回せばいいよね?って考え方っぽいです。その結果GC対象とならずに出力がfalseになったわけです。

最後にref2を見てみましょう。
これはref1と違って外の変数を参照しているラムダ式です。これではキャッシュして使いまわすことはできません。なのでキャッシュせずに毎回インスタンスを作っています。そのため他の参照がないのでGC対象になって消えます。

というわけで正解は
false
true
true
になります。

詳しい解説は弱参照とラムダ式を御覧ください。

おまけ

この問題は弱参照を使ったObserverパターンを作ろうとしたときに見つけました。
あるコンポーネントのフィールドにListenerを持っておいて、そのインスタンスを弱参照でリスナを登録するとコンポーネントが消えたときに自動的に開放されるじゃん!みたいなノリでした。
Java7までは結構うまく動いていたんですが、Java8が出てラムダ式に書き直したときに見事ハマったわけです。

Return or not return

by orekyuu 0 Comments

次のコードをコンパイルした時、コンパイルエラーが出るメソッドはどれでしょう?複数あります。

public class Main {

    String case1() {
        while (true) {

        }
    }

    private boolean case2 = true;
    String case2() {
        while (case2) {

        }
    }

    private final boolean case3 = true;
    String case3() {
        while (case3) {

        }
    }

    String case4() {
        boolean case4 = true;
        while (case4) {

        }
    }
}

解説と答え

戻り値を返さないといけない場合でも到達不可能であればreturnを省略することができます。というか逆に書くとコンパイルエラーになります。
今回の問題は到達不可能かを問う問題です。

まずcase1は直接trueが入っているのでwhileの次の行が実行されることはないので、returnを書きません。
次にcase2ですが、これはコンパイル時には判断できないのでコンパイルエラーになります。
次にcase3ではfinalになっているので変数の中身が変わることはありません。なので次の行が実行されないことが分かるのでエラーにはなりません。
case4はスコープが狭くなっていますが、finalではないためコンパイルエラーになります。

なので、コンパイルエラーになるのは「case2, case4」です。

IntelliJで寿司を回す

by orekyuu 0 Comments

はじめに

この記事はJetBrains Advent Calendar 2016の記事です。

最近Twitterではエディタで寿司を流すのが流行っているそうです。私の観測した範囲ではvimとemacsで寿司を流している方が居るみたいです。
そんな最近の流行に乗ってIntelliJでも寿司を流すことにしました。

寿司を流す方法
当然ですがデフォルトのIntelliJのままでは寿司を流すことは出来ません。なので、プラグインを自作するしか無さそうです。
今回はステータスバーに寿司を流すプラグインを自作してみることにしました。

プラグインを作る

IntelliJ Platform Pluginプロジェクトでプラグインを開発することが出来ます。詳しいことは色々な方が解説記事を書かれているのでそちらを参照されると良いと思います。
今回やることはステータスバーを拡張することです。plugin.xmlのextensionsタグの中で拡張ポイントを指定して独自の機能を追加できるので、ステータスバーを拡張できるようなポイントを探してみます。
非推奨になっていますがstatusBarComponentが使えそうです。

  <extensions defaultExtensionNs="com.intellij">
    <!-- Add your extensions here -->
    <statusBarComponent implementation="net.orekyuu.sushi.SushiStatusBarComponentFactory"/>
  </extensions>

implementationにはcom.intellij.openapi.wm.StatusBarCustomComponentFactoryを継承したクラスを指定します。
あとは、追加したいコンポーネントをStatusBarCustomComponentFactory#createComponentで返すだけです。

雑な実装ですが以下のようなコードを書きました。

public class SushiStatusBarComponentFactory extends StatusBarCustomComponentFactory {

    private Image image;
    private final int size = 20;
    private final int laneWidth = 300;
    private List<Sushi> sushiList = new LinkedList<>();
    private static Thread thread = null;
    private JPanel root;

    public SushiStatusBarComponentFactory() {
        try (InputStream sushiStream = this.getClass().getResourceAsStream("/sushi.png")){
            this.image = ImageIO.read(sushiStream);
        } catch (IOException e) {
            throw new UncheckedIOException(e);
        }

        int sushiCount = (laneWidth / size / 2) + 1;
        for (int i = 0; i < sushiCount; i++) {
            sushiList.add(new Sushi(i * size * 2 - size, laneWidth, image));
        }

        if (thread == null) {
            thread = new Thread(() -> {
                while (true) {
                    try {
                        if (root != null) {
                            sushiList.forEach(Sushi::update);
                            root.repaint();
                        }
                        thread.sleep(16);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
            thread.setDaemon(true);
            thread.start();
        }
    }

    @Override
    public JComponent createComponent(@NotNull StatusBar statusBar) {

        root = new JPanel() {
            @Override
            protected void paintComponent(Graphics graphics) {
                Graphics2D g = (Graphics2D) graphics;
                g.setBackground(statusBar.getComponent().getBackground());
                g.clearRect(0, 0, getWidth(), getHeight());
                sushiList.forEach(sushi -> sushi.drawSushi(g));
            }
        };
        root.setPreferredSize(new Dimension(laneWidth, statusBar.getComponent().getHeight()));
        return root;
    }
}

public class Sushi {
    private final Image image;
    private int x;
    private int y = 0;
    private int size = 20;
    private int laneWidth;
    private int speed = 1;

    public Sushi(int x, int laneWidth, Image image) {
        this.x = x;
        this.image = image;
        this.laneWidth = laneWidth;
    }

    public void drawSushi(Graphics2D g) {
        g.drawImage(image, x, y, size, size, null);
    }

    public void update() {
        x += speed;
        //端までいったら-sizeまで戻す
        if (laneWidth < x) {
            x = -size;
        }
    }
}

無事寿司を流すことが出来ました!

Unmodifiable – Java Puzzlers Advent Calendar3日目

by orekyuu 0 Comments
import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));
        List<String> unmodifiableList = Collections.unmodifiableList(strings);

        System.out.print(unmodifiableList.size());
        System.out.print(", ");
        strings.remove("aaa");

        System.out.print(unmodifiableList.size());
    }
}

上のコードを実行した時の結果はどれになるでしょうか?

  1. 3, 3
  2. 3, 2
  3. 実行時エラー
  4. コンパイルエラー

答え

2の3, 2が出力されます


解説

今回の問題はKotlinのListがImmutableではない!みたいな話を聞いたのを思い出して似たような問題を取り上げました。
Javadocを見るとCollections#unmodifiableListメソッドの解説にはこのように書かれています。

指定されたリストの変更不可能なビューを返します。

Collections#unmodifiableListは変更が不可能なListとしてラップするだけで防御的コピーはしていません。なので、元のリストを操作すれば変更可能なので注意してください。


解決策

不変なリストを作りたい場合は防御的コピーをするようにしてください。

import java.util.*;

public class Main {
    public static void main(String[] args) {
        List<String> strings = new ArrayList<>(Arrays.asList("aaa", "bbb", "ccc"));
        //↓新しいリストを作る
        List<String> unmodifiableList = Collections.unmodifiableList(new ArrayList<>(strings));

        System.out.print(unmodifiableList.size());
        System.out.print(", ");
        strings.remove("aaa");

        System.out.print(unmodifiableList.size());
    }
}

もしくはUnsupportedOperationExceptionで死ぬのは嫌なのでEclipseCollectionsなどを使うのが良いかと思います。

Equals Method Overloading – Java Puzzlers Advent Calendar1日目

by orekyuu 0 Comments

Equals Method Overloading

import java.util.*;

class Student {
    private int id;

    Student(int id) {
        this.id = id;
    }

    public boolean equals(Student student) {
        return student != null && student.id == id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}

public class Main {
    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student(1));
        students.add(new Student(2));

        students.remove(new Student(1));

        System.out.println(students.size());
  }
}

上のコードを実行した時の結果はどれになるでしょうか?

  1. 1
  2. 2
  3. コンパイルエラー
  4. 実行時エラー

答え

選択肢2の2が出力されます。


解説

Studentを受け取るequalsを作れば比較する時クラスの比較すれば良くない?みたいな話を聞いたのでそれを問題にしてみました。

なぜ2が呼ばれているかというとremove内で比較するときにequals(Object)のほうが呼ばれているからです。
以下のコードを考えてみます。

Student student1 = new Student(1);
Object student2 = new Student(1);
student1.equals(student2); //false

上記のコードを実行したとき、変数の型でどのメソッドを呼び出すか決めているのでObject型のequalsが呼び出されます。

では、次に問題のコードをデコンパイルしてみます。

public static void main(String[] args) {
    ArrayList students = new ArrayList();
    students.add(new Student(1));
    students.add(new Student(2));
    students.remove(new Student(1));
    System.out.println(students.size());
}

コンパイル後だと型の情報が消えてraw型になっています。
あとは内部でequalsが使用されたときObject#equals(Object)されていることがわかると思います。


解決策

今にオーバーロードしてもObject#equals(Object)が使用されるケースがあるので、必ずObject#equals(Object)をオーバーライドするようにしてください。

class Student {
    private int id;

    Student(int id) {
        this.id = id;
    }
    
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Student student = (Student) o;
        return id == student.id;
    }

    @Override
    public int hashCode() {
        return Objects.hash(id);
    }
}