IntelliJで寿司を回す

はじめに

この記事は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日目

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日目

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

弱参照とラムダ式

突然ですがクイズです!

以下のコードを実行した時の出力は何になるでしょうか?

cvlspm5vyaa02hs

  1. truetrue
  2. falsefalse
  3. falsetrue
  4. truefalse

簡単にコードの解説をするとWeakReferenceは引数に渡しているインスタンスに強参照(普通にJava書いてるときの参照)をしているものが無くなるとGC対象になるよ!ってものです。a, bには匿名クラスとラムダ式を渡して、即GCしています。

回答

cvlucqoukaazls0
答え: 4

ラムダ式に強参照してるやつがいる・・・?どういうことだ???


解説

ラムダ式は匿名クラスに変換されるわけではなく、invokedynamic命令で実行されています。どうやらそのへんで違いが出ていそうです。
ラムダ式の実装を調べるとわかりそうです。

メンションで教えてもらった資料を見ると39ページから答えが書いてあります。

  1. invokedynamic命令を使うと、どのようにクラスを作りインスタンスをどのように作るかの制御ができる
  2. ラムダ式を作るなら外部の変数を使用していない場合毎回作らなくてもシングルトンで良いのでは?

ということでWeakReferenceに渡していたインスタンスはシングルトンになっていたためGCされなかったんですね。なるほど。


ところで外部の変数を使用していない場合はシングルトンになるという話でした。つまり外部の変数を使用するラムダ式なら結果は変わるのでしょうか?


問題の元ネタ

ちなみに、問題の元ネタは匿名クラスとラムダ式と時々弱参照をクイズ形式にしたものです。
何も知らずにこの問題にハマるとびっくりするよね!

SpongeプラグインでSpringのDIコンテナを使う

MinecraftのSpongeプラグインでSpringを使ってみたのでその時のメモです。

Springを使うメリット
・テストがやりやすくなる
・AOPが使える←個人的にかなり重要

なぜAOPを使いたいか
継承では解決できない同じような処理がサーバーのプラグインでは結構出てくる(ロギングとか権限周り)
その辺をAOPで解決したい

プラグインのエントリポイント

package net.orekyuu.spring;

import org.spongepowered.api.event.Listener;
import org.spongepowered.api.event.game.state.GameConstructionEvent;
import org.spongepowered.api.plugin.Plugin;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;

@Plugin(
        id = "net.orekyuu.spring",
        name = "SpringDemo",
        version = "1.0-SNAPSHOT"
)
public class SpringDemo {

    private ApplicationContext context;

    private static SpringDemo INSTANCE;

    @Listener
    public void onServerStart(GameConstructionEvent event) {
        INSTANCE = this;
        //net.orekyuu.spring以下をComponentScanしてる
        context = new AnnotationConfigApplicationContext("net.orekyuu.spring");
    }

    public static SpringDemo getInstance() {
        return INSTANCE;
    }

}

ゲーム起動時にApplicationContextを作るだけです。
プラグイン以下パッケージをComponentScanするようにしておくと、アノテーションベースの設定ができるようになります。

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>net.orekyuu</groupId>
    <artifactId>spring</artifactId>
    <version>1.0-SNAPSHOT</version>
    <packaging>jar</packaging>

    <name>Spring</name>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <profiles>
        <profile>
            <id>release</id>
            <build>
                <plugins>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-source-plugin</artifactId>
                        <version>2.4</version>
                        <executions>
                            <execution>
                                <id>attach-sources</id>
                                <goals>
                                    <goal>jar-no-fork</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-javadoc-plugin</artifactId>
                        <version>2.10.3</version>
                        <executions>
                            <execution>
                                <id>attach-javadocs</id>
                                <goals>
                                    <goal>jar</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                    <plugin>
                        <groupId>org.apache.maven.plugins</groupId>
                        <artifactId>maven-gpg-plugin</artifactId>
                        <version>1.6</version>
                        <executions>
                            <execution>
                                <id>sign-artifacts</id>
                                <phase>verify</phase>
                                <goals>
                                    <goal>sign</goal>
                                </goals>
                            </execution>
                        </executions>
                    </plugin>
                </plugins>
                <resources>
                    <resource>
                        <directory>src/main/resources</directory>
                        <filtering>true</filtering>
                    </resource>
                </resources>
            </build>
        </profile>
    </profiles>

    <build>
        <resources>
            <resource>
                <directory>\${project.basedir}/src/main/resources</directory>
                <filtering>true</filtering>
            </resource>
        </resources>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.3</version>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.codehaus.mojo</groupId>
                <artifactId>templating-maven-plugin</artifactId>
                <version>1.0-alpha-3</version>
                <executions>
                    <execution>
                        <id>filter-src</id>
                        <goals>
                            <goal>filter-sources</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
            <plugin>
                <artifactId>maven-site-plugin</artifactId>
                <version>3.3</version>
                <dependencies>

                    <dependency>
                        <groupId>net.trajano.wagon</groupId>
                        <artifactId>wagon-git</artifactId>
                        <version>2.0.3</version>
                    </dependency>
                    <dependency>
                        <groupId>org.apache.maven.doxia</groupId>
                        <artifactId>doxia-module-markdown</artifactId>
                        <version>1.6</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <artifactId>maven-release-plugin</artifactId>
                <version>2.5.1</version>
                <configuration>
                    <autoVersionSubmodules>true</autoVersionSubmodules>
                    <tagNameFormat>@{project.version}</tagNameFormat>
                    <scmCommentPrefix xml:space="preserve">[RELEASE] </scmCommentPrefix>
                    <goals>install deploy site-deploy
                    </goals> <!-- install is here to fix javadoc generation in multi-module projects -->
                    <releaseProfiles>release</releaseProfiles>
                </configuration>
            </plugin>

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <artifactSet>
                                <excludes>
                                    <exclude>junit:junit</exclude>
                                </excludes>
                            </artifactSet>
                        </configuration>
                    </execution>
                </executions>
                <configuration>
                    <relocations>
                        <relocation>
                            <pattern>org.apache</pattern>
                            <!--org.apacheパッケージのクラスをforgeが読み込んでくれないっぽいので回避-->
                            <shadedPattern>org.aapache</shadedPattern>
                        </relocation>
                    </relocations>
                </configuration>
            </plugin>
        </plugins>
    </build>

    <repositories>
        <repository>
            <id>spongepowered-repo</id>
            <url>http://repo.spongepowered.org/maven/</url>
        </repository>
    </repositories>

    <dependencies>
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.spongepowered</groupId>
            <artifactId>spongeapi</artifactId>
            <version>4.1.0-SNAPSHOT</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>4.3.2.RELEASE</version>
            <exclusions>
                <exclusion>
                    <groupId>commons-logging</groupId>
                    <artifactId>commons-logging</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.7.5</version>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>logback-classic</artifactId>
            <version>1.0.13</version>
        </dependency>

        <!--AOP-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-aop</artifactId>
            <version>4.3.2.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>RELEASE</version>
        </dependency>

    </dependencies>

    <reporting>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-javadoc-plugin</artifactId>
                <version>2.10.3</version>
            </plugin>
        </plugins>
    </reporting>
</project>

これがpom.xmlです。
注意としてはそのままflat-jarにしてしまうとClassNotFoundExceptionが発生してしまいます。
原因はMinecraftのLaunchClassLoaderにあります。このClassLoaderでModが読み込まれるのですが、org.apache.logging以下パッケージが無視されています。正確にはLaunchClassLoader.class.getClassLoader()でロードされるのですが、LaunchClassLoaderとは親子関係がなくて見つからないみたいな流れ?

そのため解決策としてmaven-shade-pluginを使ってorg.apacheパッケージをリネームすることで回避しました。

イベントをSpringで
Spongeで発生するイベントをSpringのイベントに置き換えてみます。

@Component
public class EventConverter {

    @Autowired
    private ApplicationEventPublisher publisher;

    @Listener
    public void onSpongeEvent(Event event) {
        //SpongeのイベントをSpringに流す
        publisher.publishEvent(new SpongeEvent<>(SpringDemo.getInstance(), event));
    }
}

このComponentはSpongeのイベントをすべて受け取り、Springのイベントへ変換してイベントを通知します。

public class SpongeEvent<T extends Event> extends ApplicationEvent {

    private final T event;

    public SpongeEvent(Object source, T event) {
        super(source); //とりあえず適当に渡しておく
        Objects.requireNonNull(event);
        this.event = event;
    }

    public T getEvent() {
        return event;
    }
}

Spongeのイベントを持つだけのラッパークラスを作りました。ここは特に解説は必要ないと思います。

次に受け取り側です。

//プレイヤーがサーバーに入った時のイベントを受け取ってなにかする
@Component
public class PlayerJoinEventListener {

    @Autowired
    public PlayerMessageService playerMessageService;

    @EventListener
    public void handleOnPlayerJoin(SpongeEvent<ClientConnectionEvent.Join> event) {
        playerMessageService.sendHelloMessage(event.getEvent().getTargetEntity());
    }
}

Playerが入ってきた時のイベントを取ってみました。
受け取りたいイベントを引数にとって、EventListenerアノテーションをつけるだけです。

Spongeのイベント以外にも独自のイベントを作りたい場合はApplicationEventを継承したクラスを作り、ApplicationEventPublisherへ流すだけです。
詳しく知りたい方はこちらを見るのが良いかと思います。
https://spring.io/blog/2015/02/11/better-application-events-in-spring-framework-4-2

最後に設定です。

@Configuration
public class EventConfig {

    @Autowired
    private EventConverter eventConverter;

    //EventConverterをEventManagerに登録する
    @Bean
    public EventManager eventManager() {
        EventManager manager = Sponge.getEventManager();
        manager.registerListeners(SpringDemo.getInstance(), eventConverter);
        return manager;
    }

}

SpongeのEventManagerにEventConverterを登録しているだけです。

SpringAOPでロギング
AOPの定番ネタですがロギングです。

まずは設定から

@Configuration
@EnableAspectJAutoProxy
public class AopConfig {
}

はい。EnableAspectJAutoProxyしてるだけです。

次にロギングのインターセプタを作ります。

@Component
@Aspect
public class LogInterceptor {

    private static final Logger logger = LoggerFactory.getLogger(LogInterceptor.class);
    @AfterReturning("execution(public * net.orekyuu.spring.service..*.*(..))")
    public void printLog(JoinPoint joinPoint) {
        logger.info("ServiceLog: " + joinPoint.getSignature());
    }
}

net.orekyuu.spring.serviceパッケージ以下にある全てのpublicなメソッドのどれかが正常終了した時にログを出力しています。
ポイントカット式とかの書き方は以下を見ればよいかと。
http://docs.spring.io/spring/docs/current/spring-framework-reference/html/aop.html

やりたい設計とか
Controller→Service→Repositoryの構造になんか近いような・・・?
EventListener→Service→Repositoryって結構よさ気では!?

テストとかDBに関してはまた後日

痛IntelliJを作ろう

IntelliJを痛くしろという神の啓示を受けたので、痛くしてみました。

intellij

今回は上のようなエディタを作ってみます。

今まではSexy Editorというプラグインを使う方法がありましたが、2016.2からは”Set Background Image”というアクションが追加され、そこから設定画面を開いて背景画像を設定できるようになりました。

このアクションは上のメニューバーには見つからなかったので、Find Action(WinならCtrl+Shift+A)からSet Background Imageというアクションを検索して呼び出します。

background_image

明るい写真だと文字が見づらいので透明度を下げて調整します。
これで設定は完了です。

それでは良い痛IntelliJライフを!

JJUG CCC 2016 springに行ってきた

Type safe Annotation #ccc_gh1

桜庭さんのタイプアノテーションの話からスタート
JSR-305はずっと通っているものだと思っていたんだけど、ステータスが休止中になっているのをその時知って1年ぶりの衝撃を受けました。
そこで気づいて急いで発表予定のスライドを修正するなんてことになったり。
あとChecker Frameworkなんてものがあるの知らなかったので使ってみたいと思います。

Thymeleaf3を使ってみよう! #ccc_f2

Spring祭りその1。
去年からSpringBootでThymeleafを使っていて、次作るWebアプリでも使う予定なので聞きに行きました。
今までWebデザイナの人にhtml書いてもらうとThymeleafで「brタグが閉じられてない」みたいなエラーが出ていたので、それが次のバージョンからなくなるって話がすごい嬉しかったです。
逆に言えばそれ以外はめぼしい機能はないのかな?という印象。

SpringBootでBootした後に作るWebアプリケーション基盤 #ccc_e3

Spring祭りその2。
SpringBootを入門読んでHelloWorldした後に読むべき資料!という感じでうまくまとまっていて良いセッションでした。
学校の人に見せたい資料です!

テストゼロからイチへ進むための戦略と戦術 #ccc_e4

テストゼロの状態からテストを書くまでの話。
テストほぼ書いていない人間だったので結構耳が痛くなる話でした。テスト書かねば・・・!
コードのゴミ掃除で1コミット1.7万行削除には笑いましたw

Spring Framework/Bootの最新情報とPivotalがすすめるクラウドネイティブなアプローチ #ccc_gh5

Spring祭りその3。
バナーがついに画像に対応!
絶対にアピールする点が間違っている気がwww
SpringBoot1.4から@GetMapping@PostMappingのようなアノテーションが増えてちょっとだけ楽になりました。いいぞ〜!

OpenJDK コミュニティに参加してみよう #ccc_i62

OpenJDKのコミュニティに参加する方法の解説。
プロジェクトが大量にあったり、どうやり取りすればいいんや!みたいな一歩目の敷居が高そうに感じるところをうまく解説されてました。
このセッション終わったあとnokoに「明日にはコミッタになってるんだよね?ん?ん?」みたいな不当な煽りを受けました。

古のJavaを使うということ #ccc_m71

J2SE 1.4の環境で働いている方のセッション。
テストコードを書こうと思っても上司が納得しなかったり、不要なコメントを削除するのにも苦労するという話を聞いて辛さが溢れてました。
環境が改善されると良いですね・・・。

SpringBoot+Kotlin 劇的ビフォーアフター #ccc_i72

僕のセッションです。
緊張で結構カミカミになりましたけど。大体良い時間で話しきれて良かったです。
cccの当日にイケてないところの解決策が出てきたりして資料修正したりしてましたが、解決策を知れるJJUG CCC最高!というかんじでした。

IntelliJ IDEAでSteamブロードキャスト

突然ですが最近ItelliJ IDEAというゲームにはまってます。

最近SteamでIntelliJ IDEAのプレイ風景を配信できることに気付いたので方法を紹介します。

続きを読む

4/2(土) Kotlin 1.0リリース記念勉強会 in 京都に行ってきた

Kotlinの勉強会にSpringBoot+Kotlinネタでしゃべりにいきました。
発表したスライドはこちらになります。

会場でAndroid以外でKotlinを使ってる人がどれくらい居るか質問したんですが、予想よりも多くて驚きました。
Androidアプリで「Java7までしか使えないからKotlin使うかー。使うしか無いかー。」みたいな使われ方するのがメインだと思ってたんですけど、普通にJavaの置き換えとして使うみたいな人が多いんですかね?

続きを読む