vaguely

和歌山に戻りました。ふらふらと色々なものに手を出す毎日。

java.lang.reflect.Proxy に触れてみる

はじめに

Spring Data JPA プログラミング入門」を読み始めたのですが (n 回目)、その中にこのような話がでてきます。

DB へのアクセスにはリポジトリが必要 -> そのリポジトリは class として実装を直接書くのではなくのではなく、 interface を用意する -> Spring Framework 側で proxy を使ってその interface を実装したクラスを生成する

あーそーゆーことね。完全に理解した(わかってない)。

ということで、 java.lang.reflect.Proxy について調べてみることにしました。

なおタイトルなどで 「java.lang.reflect.Proxy」 とわざわざ言っているのは、会社で悩まされがちなプロキシサーバーとの区別をつけるためです。

ただし面倒なのでこれより下は Proxy と書くことにします。

Proxy について

まずは Proxy について、ドキュメントを見てみましょう。

あーry

気を取り直して、 Proxy の動きを一言にまとめると、対象の class が持つメソッドが実行されたことを、その class が継承している interface を通して取得できるようにする、といったものです。
(私の理解では)

これによって、メソッドが実行される前後に別の処理を追加することが可能になります。

Sample

まずは動作を確認するため、 改訂2版 パーフェクトJava のサンプルを参考にコードを書いてみます。

まずは元の interface と class から。

IProxySample.java

package jp.masanori;

public interface IProxySample {
    void say();
    String getName();
    void setName(String name);
    private void sayInPrivate(){
        System.out.println("private");
    }
    default void callName(){
        System.out.println("default");
        sayInPrivate();
    }
}

ImplementProxySample.java

package jp.masanori;

public class ImplementProxySample implements IProxySample {
    @Override
    public void say(){
        System.out.println("hello world");
    }
    @Override
    public String getName(){
        return "masanori";
    }
    @Override
    public void setName(String name){
        this.name = name;
    }
}

で、これが今回の話の中心となる Proxy を取得するクラスです。

ProxySampleClass.java

package jp.masanori;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;

public class ProxySampleClass implements InvocationHandler {
    private final Object targetClass;
    public ProxySampleClass(Object targetClass) {
        this.targetClass = targetClass;
    }
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // 対象のメソッドを実行する前に処理を追加.
        System.out.println("before invoking method.");
        // 対象のメソッド.
        Object o = method.invoke(targetClass, args);
        // 対象のメソッドを実行した後に処理を追加.
        System.out.println("after invoking method.");
        return o;
    }
}

Proxy を呼び出して利用するコードです。

App.java

package jp.masanori;

import java.lang.reflect.Proxy;

public class App {
    public static void main( String[] args ) {
        // Proxy を取得して IProxySample にキャストする.
        var proxy = (IProxySample)Proxy.newProxyInstance(
                App.class.getClassLoader(),
                new Class[]{ IProxySample.class },
                new ProxySampleClass(new ImplementProxySample()));

        System.out.println("start");
        proxy.say();
        System.out.println("after say");

        proxy.setName("masanori");
        System.out.println("after setName");

        System.out.println("My name is " + proxy.getName());

        proxy.callName();
        System.out.println("after callName");
    }
}

実行結果は下記の通りです。

start
before invoking method.
hello world
after invoking method.

after say

before invoking method.
after invoking method.
after setName

before invoking method.
after invoking method.
My name is masanori

before invoking method.
default
private
after invoking method.

after callName

結果からは下記のようなことがわかります。

  • 対象 interface の全 public メソッドが実行されるときに ProxySampleClass.javainvoke が呼ばれる(含 default)。
  • privateメソッドでは invoke が呼ばれない。
  • 戻り値を使って何か行う処理は、 invoke で追加された処理の実行後に実行される。

invoke について

ProxySampleClass.javainvoke で渡される引数についてです。

第二引数の method について、一応 method.getName() で Invoke されたメソッド名が取れるので、条件分岐して特定のメソッドのみ処理を追加する、といったことも可能ですが、複雑になるので特定の場合のみ実行したいなら呼び出し元で対応したほうが良いような気はします。

また method.invoke の第一引数は、Proxy で処理を追加したい interface を実装する class である必要があります。

第三引数は メソッドの引数がそのまま渡され、引数がない場合は空の配列が渡されます。

Proxy.newProxyInstance について

  • Proxy.newProxyInstance の第一引数となる getClassLoader() は、 Main でも Proxy 対象となる class でも OK のようです。
  • newProxyInstance の戻り値で getClass をすると、キャスト後でも com.sun.proxy.$Proxy0 が返ります。
  • 第二引数の class 配列には interface.class を指定する必要があり、例えば実装 class を使おうとすると IllegalArgumentException が発生します。
  • 第三引数には InvocationHandler を継承した class を渡し、そのコンストラクタには Proxy 対象となる class を渡します(メソッドが呼ばれたときに、元のメソッドを実行するのに使用)。
  • 第二引数に渡す interface は、 Proxy 対象となる class が継承していないものを渡すこともできます。
  • またキャストしないのであれば第二引数に空配列を渡しても OK です。あまり意味はないと思いますが。

Proxy が継承した interface (第二引数で渡している interface )は getInterfaces で取得できます。

for(var c : proxy.getClass().getInterfaces()){
    System.out.println(c);
}

Spring Data JPA の repository のコードを見てみる

最初の話に戻って、Spring Data JPA の repository のコードを見てみることにします。

Spring Framework の GUIDES のサンプルを見て、 Proxy がどのように使われているのか追ってみることにします。

https://spring.io/guides/gs/accessing-data-jpa/

Customer という Entity クラス(ドメインオブジェクト)に対して CustomerRepository という Repository (interface) が用意されています。

で、メインクラスである Application.java で CustomerRepository を継承した Proxy が渡される、という内容になっています。

Application.java

package hello;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.Bean;

@SpringBootApplication
public class Application {
    ~省略~
    @Bean
    public CommandLineRunner demo(CustomerRepository repository) {
    return (args) -> {
    ~省略~
    }
  }
}

この Proxy 、デバッガのブレークポイントを止めて見てみると、 org.springframework.data.jpa.repository.support.SimpleJpaRepository という class に対して生成されているようです。

https://github.com/spring-projects/spring-data-jpa/blob/master/src/main/java/org/springframework/data/jpa/repository/support/SimpleJpaRepository.java

このクラスから EntityManager にアクセスしているわけですが、 Spring徹底入門 によると、処理の流れは下記のようなものとのことです。

  1. Spring Data JPA が CustomerRepository を継承した Proxy を生成する。
  2. SimpleJpaRepository に 1. の処理を委譲する。

で、実際の動きを見てみると、 SimpleJpaRepository に CustomerRepository のメソッドが追加されたような状態になっていて、これまで触れてきた Proxy の動きとは少し違っているようですね。

Mixin

Proxy を使って実現可能なこととして、 InvocationHandler の対象クラスが実装していないメソッドを追加する (Mixin) 、というものがあるそうです。

  • Javaにおける動的Mixin - Qiita

  • 追加したいメソッドを、 interface でデフォルトメソッドとして書いておく。

    1. を Proxy に継承させる。
  • そのまま呼び出すと実装クラスに該当メソッドがなくエラーになるため、 (InvocationHandler の) invoke でバインドする。

※下記のコードはあくまで特定の条件においてうまく動作することだけを確認したもので、エラー処理などが抜けています。
正しくは下記リンクをご覧ください。

https://qiita.com/kawasima/items/f735ef0c0a9fa96f6eb4

https://github.com/kawasima/enkan/blob/master/enkan-core%2Fsrc%2Fmain%2Fjava%2Fenkan%2Futil%2FMixinUtils.java

App.java

package jp.masanori;

import java.lang.reflect.Proxy;

public class App { public static void main( String args ) { // ImplementProxySample で継承していない IOtherSample を持った Proxyを生成する. var proxy = (IOtherSample)Proxy.newProxyInstance( App.class.getClassLoader(), new Class<?>{ IProxySample.class, IOtherSample.class}, new ProxySampleClass(new ImplementProxySample())); // IOtherSample のデフォルト実装を呼び出す. proxy.sayMessage(); } }

IOtherSample.java

package jp.masanori;

public interface IOtherSample {
    default void sayMessage(){
        System.out.println("hello everyone");
    }
}

ProxySampleClass.java

package jp.masanori;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Field;

public class ProxySampleClass implements InvocationHandler {
    ~省略~
    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        // method実行の後に処理を挟まないなら直接ReturnしてもOK.
        Object result;
        if (method.getDeclaringClass().isAssignableFrom(targetClass.getClass())) {
            result = method.invoke(targetClass, args);
        }
        else{
            // methodを定義しているクラス(IOtherSample)を取得.
            final Class declaringClass = method.getDeclaringClass();
            // methodの参照(MethodHandle)のファクトリであるLookupの取得.
            MethodHandles.Lookup lookup = MethodHandles.publicLookup()
                    .in(declaringClass);

            // そのままlookup.unreflectSpecialで対象methodのMethodHandleを取ろうとすると,
            // IllegalAccessExceptionが発生するのでallowedModesの値を変更.
            if (Modifier.isFinal(modifiers)) {
                  final Field modifiersField = Field.class.getDeclaredField("modifiers");
                  modifiersField.setAccessible(true);
                  modifiersField.setInt(f, modifiers & ~Modifier.FINAL);
                  f.setAccessible(true);
                  f.set(lookup, MethodHandles.Lookup.PUBLIC | MethodHandles.Lookup.PRIVATE);
            }
            // アクセス可能になったらProxyにMethodHandleをバインドしてmethodを実行する.
            MethodHandle methodhandle = lookup.unreflectSpecial(method, declaringClass);
            result = methodhandle.bindTo(proxy)
                    .invokeWithArguments(args);
        }
        return result;
    }
}

  • Proxy 対象の class が該当の method を定義している場合、 Mixin の処理を実行するとエラーになるため、isAssignableFrom で確認しています。
  • method にデフォルト実装がない場合は java.lang.AbstractMethodError が発生します。
  • ProxySampleClass のコメントにも書きましたが、 MethodHandle を取得して Proxy にバインドするためには Private アクセス特権が必要で、そのままだとエラーが発生します。
  • 上記の通りエラー処理などは含まれていないため、実際にはリンク先を確認するなどして正しくエラーが処理されるようにしてください。

正直ほとんどよくわからないままではあるのですが、 SimpleJpaRepository でもこれと同じような処理が行われているものと考えられます。

Java に限らず Reflection や Proxy を触りまくる、ということは普段はないかもしれませんが、また新たな世界が垣間見れたような気がします。

参照

Proxy

Mixin

英語でLightning Talk

はじめに

5/14に行われた Oracle Dev Tour Japan in Osaka で LT をさせていただきました。

kanjava.connpass.com

speakerdeck.com

初の英語での LT ということで、日本語とは違うところがあったので準備の話などとともに書き残しておくことにします。

なお一応書きますが、英語で LT やるコツとか出てきません。

僕が知りたいくらいです。

発表について

まぁ完全に準備不足であったと思います。

大きく2点あって、原稿は作ったもののそれを声に出して読む、という練習が足りなかったと思います。

原稿を忘れてしまっても、その場で作りながら話をする、ということが理想ではあるのですが、まだそこまで消化できていなかったのだと思います。

家だとどうしても子どもたちが寝ていたり横やりが入ったりと難しいので、広い公園やカラオケボックスなど練習できる場所の確保が必要ですね。

もう一つがスライドで、内容自体は自分なりにわかりやすくなるよう作ったつもりですが、発表のときに(ディスプレイが複製モードになっていなかったため)自分のPCのモニタから見えず、カンペ代わりにできなかったという問題がありました。

Office というかパワポについては、やっぱり MS Office 良いなぁ、という感じがします。
(スクリーン側に全画面でスライドを表示して、PC側でメモや経過時間などを見ることができるため)

家のディスプレイを繋いでの確認もやっておかないといけませんね。

なお Ubuntu でディスプレイの設定を変更するには、 Settings > Devices > Displays からできる。。。ハズ。

ディスプレイは Mac Mini 用に一台あるのですが、ケーブルが HDMI と DVI ケーブルしかなくて VGA が必要な PC につなげませんでしたorz

明日か明後日に買おっと。

英語で原稿を書くことについて

日本語で書くようにスラスラと英語が出てきてくれるのが理想ですが、これも現状では難しいので、 Google 先生に頼ることにしました。

前職で韓国語に翻訳する必要があったりしたときに使っていた方法ですが、日本語を韓国語に翻訳 -> 翻訳結果を日本語に再翻訳して意味が通るかを確認してました。

英語は多少は書ける(気がする)ので、単語は調べつつ英文を作る -> 日本語に翻訳して、おかしな日本語になっていたらやり直ししたりしていました。

あと発音がわからないときに、これまた Google 翻訳の発声機能を使って確認できたのも助かりました。

この方法で、英語のブログに挑戦してみるのも良いかもしれませんね。

原稿

せっかくなので用意していた原稿も載せておきます。


Start

Ok, Let's start.

I'm Masui Masanori.

Today I'll talk about "var".

Have you already used it?

about Java's 'var'

  • The official name is 'Local-Variable Type Inference'.
  • It infers the type from the right side value.
  • If the type can't be inferred, the compiling error is occured.
  • You should take care what type is inferred. Because the type sometimes differ from your expected.
  • It can declare the variable of anonymous class.

about C#'s 'var'

  • C# also has 'var'.
  • Most of the features are as same as Java's.

Question

  • I felt C# programmers are more actively seeking to use it than Java programmers.
  • Where does the difference come from? and should I use it?

Merit of using 'var'

First, I talk about the merits of 'var'.

  • It improve readability. Because it make the code simpler.
  • It induces better naming. Because the type name is omitted from left side, so the programmer will seek to compensate the infomations with other parts.
  • If the precise type isn't so important, You can treat the type ambiguous. For example in this code. Even if the returning value type is changed to array of string, you don't need any modifications.

Demerit of using 'var'

Next, I talk about the demerits. * It reduces readability. For example in this code, I don't know what type will be returned. And even if I can understand the type, you shouldn't name like this.

  • It risks type mismatch. The biggest problem is I may not be able to know the type mismatch because the compiling error isn't occured.

Where does the difference come from?

  • This situation is not much different between Java and C#.
  • One of the big difference is that C#'s 'var' was implemented since over 10 years ago, so the programmers have been used to using it.
  • I think maybe this is the reason.
  • I don’t know if the situation of Java also will be as same as C#'s.

Should we use ‘var’ ?

I move another question.

I think we should use ‘var’ if there is no special reason.Like the examples what I talked.

Because keeping code simple makes us to think other important parts of code.

And the most important reason of my opinion is my favorit IDEs of C# suggest using it.

Summary

  • Because ‘var’ has some merits and demerits, so please use it properly.​
  • If the variable needs the precise type, please write the type explicitly.​
  • If your team has coding rules, please follow them or update them first.​

Rerences

End

Thank you for listening.


おわりに

正直なところもっと練習しておけば。。。と思い頭をかきむしりたくなる気持ちを抑えつつこの記事を書いているわけなのですが、挑戦したこと自体はとても良かったし、貴重な経験だったと思っています。

ということで、そんな貴重な場を提供していただいた関ジャバの方々と Oracle Dev Tour Japan の方々、何より温かく見守ってくださった皆様、本当にありがとうございました(..)_

いつかどこかでリベンジも狙っていきます(๑•̀ㅂ•́)و✧

Java と C# の var をぼんやり比較

はじめに

みなさん Java10 使ってますか〜?(何)

Java10 で特に注目された(多分)新機能に、 var の導入があります。

var については後述しますが C♯ では ver.3.0 から導入されていたりします。

ただ C♯ では使えるところでは使う、位の扱いになっている(個人の感想です)のに対し、Javaではもう少し控えめに使おう、 という感じを受けました。

まぁJavaでは登場したばっかりだったり、別の型推論があったりと状況が違うところはあるためあくまで現状、 ということにはなりますが。

とはいえ、この違いはどこから来るのだろう?と思ったので調べてみることにしました。

var について

まずは var についてですが、ローカル変数の定義において、左辺に型を明示しなくても右辺の値から型を推論してくれる、というものです。

var id = 0; // int 型として扱われる.

int や string くらいならともかく、10 文字 20 文字と長くなってくると見る方も辛くなってきますね。

そのような場合でもスッキリ書ける、という利点があります。

  • JavaScript などとは異なり、 var を使うからといって定義後に別の型に変更することはできません。
  • コンパイル時に推論された型に置き換わります。
  • 右辺の型から推論するため、「var e;」 や 「var n = null;」 のようにするとエラーになります。

匿名クラス

また var が必須となる場合として、匿名クラスがあります。

Java

var person = new Object() {
    private String name;
    private int id;
    public String getName(){
        return name;
    }
    public void setName(String name){
        this.name = name;
    }
    public int getId(){
        return id;
    }
    public void setId(int id){
        this.id = id;
    }
};
person.setName("masanori");
person.setId(0);

System.out.println("name: " + person.getName() + " id: " + person.getId());

C♯

var person = new
{
    Name = "masanori", Id = 0,
};
Console.WriteLine("name: " + person.Name + " id: " + person.Id);

Java ではメソッドの定義が可能だったり、定義後の変数の変更が可能だったりと、言語仕様上の違いはありますが、いずれも var を使わずに定義しようとするとエラーになります。

Java は 「new Object」 となっているので一見 Object 型かな?とも思ってしまいますが。

var が使えない場合

ローカル変数であれば常に var が使えるかというと、そうではありません。
いくつかの状況ではコンパイルエラーになります。

ラムダ式 (Java, C♯)

下記のようなラムダ式では右辺の方が確定しないので var を使うことはできません。

// Compile error.
var lambdasample = () => {
   // Do something.
};

Java

var lambdasample = (Runnable)(() -> {
   // Do something   
});
Runnable lambdasample2 = ()->{
   // Do something
};

C♯

// OK
var lambdasample = (Action)(() => {
    // Do something   
});

// OK
Action lambdasample = () => {
   // Do something
};

配列の定義 (Java, C♯)

配列を定義する場合、下記のように書くとコンパイルエラーとなります。

// Compile error.
var arraySample = {0, 2};

理由は型が int[] に限定できないため、ということのようです。

Java

// OK.
var arraySample1 = new int[]{0, 2};
// OK.
int[] arraySample2 = {0, 2};

C♯

// OK. "int"は書かなくてもOK.
var arraySample1 = new []{0, 2};
// OK.
int[] arraySample2 = {0, 2};

C♯の arraySample2 で int が推論されるなら、 arraySample も良さそうな気がしないでもないですが。

継承しているクラス・インターフェースとして定義する (C♯)

例えば定義しようとしている変数が、このようなインターフェースを継承したクラスであった場合。

ISample.java

public interface ISample {
    String getName();
}

SampleClass.java

public class SampleClass implements ISample{
    private String name;

    SampleClass(String name){
        this.name = name;
    }
    public String getName(){
        return name;
    }
}

var を使うと、インターフェースではなくそれを実装している SampleClass として定義されます。

var sample = new SampleClass("hello");

ISample i = sample;
SampleClass s = sample;

// SampleClassとして定義される.
var s2 = sample;

この例で見るとそんなに大きな問題はないのですが、(C♯の) foreach で var を使った場合に思ったメソッドが出てこなくて???となる、といったことがまれによくありますね。

また C♯ で、(おそらく)上記の理由により、下記で SampleClass 、AnotherSampleClass がともに同じインターフェース (ISample) を継承していても下記のように書くことはできません。

// CompileError.
var samples = new[] {
    new SampleClass(1, "1"),
    new AnotherSampleClass(2, "2"),
};

// OK
ISample[] samples2 = {
    new SampleClass(1, "1"),
    new AnotherSampleClass(2, "2"),
};

// OK
var samples3 = new ISample[]{
    new SampleClass(1, "1"),
    new AnotherSampleClass(2, "2"),
};

ダイアモンド演算子 (Java)

Java7 で導入されたダイアモンド演算子と var を一緒に使うことはできません。

正確には使うこと自体は可能ですが、下記の場合だと型が ArrayList< Object> になってしまいます。

// ArrayList< Object>
var sampleList = new ArrayList< >();

// List< String>
List< String> sampleList2 = new ArrayList< >();

まぁダイアモンド演算子が左辺から型を推論するものである以上当然とも言えますが。

var をいつ使うべきか

では、使えるところではすべて var を使うべきでしょうか。
これは C♯ でも議論が行われていたようですし、今 Java で行われていたりもします。

で、先に自分の考えを書いてしまうと、使えるところではどんどん var を使って良いと思っています。

var を使うメリット・デメリット

とはいえまず var を使うメリット・デメリットから。

varを使うメリット

  1. 特に型名が長い場合も簡潔に書ける
  2. 変数の型を変えたい場合の修正量が少なくなる
  3. メソッドの戻り値が分かっていなくても書ける
  4. コードを書く量が少なくなる

varを使うデメリット

  1. 型の情報が失われる (特に IDE ではなく Web 上でコードを見た場合に調べづらい)
  2. 呼び出しているメソッドの戻り値が変更された場合に気づきにくい

メリット 3. は特にあまり使ったことのないメソッドなどを調べたりするのに便利ですね。

メリット 4. は他の方の意見通り、 IDE を使っているなら旨味は少ないかもしれません。
ただ、メソッド名まで型を気にせずに書ける、という点は利点かもしれません。

ドキュメントでの扱い

ドキュメントでは var をどのように使うべきと書かれているかを見てみます。

Microsoft Docs では下記のように書かれています。

  • 変数の型が割り当ての右側から明らかである場合、または厳密な型が重要でない場合は、ローカル変数の暗黙の型指定を使用します。
  • 割り当ての右側から型が明らかではない場合、var を使用しないでください。
  • 変数の型を指定するときに変数名に頼らないでください。 変数名が正しくない場合があります。
  • dynamic の代わりに var を使用しないようにしてください。
  • for ループおよび foreach ループでループ変数の型を決定するときは、暗黙の型指定を使用します。

引用元: C# のコーディング規則 (C# プログラミング ガイド) - Microsoft Docs

一方の Java についてですが、 OpenJDK の Style Guidelines を見てみます。

なんちゃって翻訳のため、詳しい内容や原文などは下記をご覧ください(..)_

変数名

var を使うことで、その変数についての左辺の情報は減少します。

それだけに適切な変数名をつける、ということがより重要になってきます。
前述のドキュメントでも触れられていますし、 var を使うべきか、という話でほぼ必ず登場します。

良い変数名を付けることを強制できることがメリット、という意見もあれば

変数名などで変なルールを作るべきでない、という話もあります。

これについては後者に賛成しますが、 var を使う・使わないに限らずわかりやすい名前を付けることは大事ですね。

型の詳細

var を使う・使わないを考える上でもう一つ重要なことが、その変数を使うにあたって型の詳細が必要かどうか、ということだと思います。

例えば C♯ でメソッドを呼び出して、その戻り値を foreach で使いたい場合、それが IEnumerable でも List でも配列でも特に問題はないと思います。

public void DoSomething()
{
    // samplesがListや配列でもOK.
        var samples = GetSamples();
    foreach (var s in samples)
    {
        // 何かの処理.
    }
}
private IEnumerable GetSamples()
{
    return Enumerable.Range(0, 10)
        .Select(n => new SampleClass(n, "masanori " + n));
}

このような場合、 var を使ったとしても可読性は下がらないのではないでしょうか。
(もしくは型を明示したとしてもあまり可読性が向上しないように思います)

一方、 float だと思っていたものが int だった場合など、型が異なると問題が発生する場合もあります。

とりわけ Effective C♯ で指摘されている、メソッドの戻り値が変更されてしまう危険性がある場合は、型名を明示しておいた方が良いかもしれません。

ということでこの項の最初の話に戻りますが、私の考えとしては、基本的には var を使い、型が正確に一致していないと問題が発生する場合や、インターフェース型として扱いたいなど必要がある場合のみ型を明示する、という感じです。

こうすることで、厳密な型が必要な箇所とそうでもない箇所のメリハリもつく気もしますし。

あと、var を使う・使わないにかかわらずその変数がどのように振る舞うのかは正しく認識しておく必要がありますね。

おわりに

結局 C♯ と Java でなぜ捉え方が違っている(ように感じる)のか、という話については、仕様として導入されてからの時間によっているような気がします。

2011年とか昔の記事では、 C♯ でも var の使用に否定的な意見も見られたので。
(C♯ では ver.3.0 に導入された話なので、今更話題にもなりにくいというのはあると思いますが)

また個人的には、 JetBrains の ReSharper や Rider がデフォルトで積極的に var をおすすめしてくるのも大きな要素だとは思いますw

今後 IntelliJ IDEA がどうなるのか、 Java が C♯ と同じような状況になるかはわかりませんが。

途中にも出てきましたが、 Java にせよ C♯ にせよ、 var を使うことが必ずしも最良、というわけではありません。

ただ、シンプルにできるところはできるだけ簡素化し、重要なところに力を注げると良いなとは思います。

参照

C♯

Java

Javaのtry-with-resourcesとC#のusingを比較する その2

はじめに

前回の続きです。Java の try-with-resources と C# の using の共通点、相違点をダラダラと見ていきますよ。

どう展開されるか

try-with-resources と using は、それぞれ内部的にはどのように展開されるのでしょうか。

まずは Java の class ファイルを見てみます。

App.java

public class App{
    public static void main(String[] args){

        try (BaseSampleClass s1 = new BaseSampleClass("class1-1")){
            System.out.println("try1-1");
        }
        try (BaseSampleClass s1 = new BaseSampleClass("class1-2")){
            System.out.println("try1-2");
        }catch (Exception e){
            System.out.println("catch1-2");
        }
    }
}

App.class

public class App {
    public App() {
    }
    public static void main(String[] args) {
        BaseSampleClass s1 = new BaseSampleClass("class1-1");
        Throwable var2 = null;

        try {
            System.out.println("try1-1");
        } catch (Throwable var19) {
            var2 = var19;
            throw var19;
        } finally {
            $closeResource(var2, s1);
        }

        try {
            s1 = new BaseSampleClass("class1-2");
            var2 = null;

            try {
                System.out.println("try1-2");
            } catch (Throwable var16) {
                var2 = var16;
                throw var16;
            } finally {
                $closeResource(var2, s1);
            }
        } catch (Exception var18) {
            System.out.println("catch1-2");
        }
    }
}

try のみを書いた場合、 try-catch を書いた場合とで結果が異なっていますね。

C# では、 using のみを使用した場合 (try を使わなかった場合)、 try-finally が使用されています。

参照

ラムダ式における AutoCloseable(IDisposable)

次のコードは Java では問題ありませんが、 C# ではコンパイルエラーになります。

Java

// OK
try(IntStream nums = IntStream.range(0, 100)
        .map(n -> n)
        .filter(n -> n % 2 == 0)){
    // 何かの処理.
}

C♯

// CompileError
using (var nums = Enumerable.Range(0, 100)
    .Select(n => n)
    .Where(n => n % 2 == 0)){
    // 何かの処理.
}

これは、 IntStream が継承している BaseStream では AutoCloseable を継承しているのに対し、 Enumerable は IDisposable を継承していないためです。

では Stream API を使うときは、いつも try-with-resouces が必要なのか?というと、そうではないようです。

ドキュメントによると、基本的には close の必要はなく、下記のファイル操作のような、Stream API でなくてもCloseが必要なリソースを扱う場合のみ必要になるようです。

Path p = new File("test.txt").toPath();

try (Stream s = Files.lines(p, StandardCharsets.UTF_8)){
    s.forEach(System.out::println);
}
catch (IOException e){
    System.out.println(e.getLocalizedMessage());
}

なお上記のコードは C# だと以下のようになります。

File.ReadAllLines(@"test.txt", Encoding.UTF8)
    .ToList()
    .ForEach(Console.WriteLine);

あれ? using は?となりそうですが、 ReadAllLines を見てみると using が使われており、
自分では書かなくて良いよ〜、ということのようです。

2018.4.15 8:00更新

ReadAllLines は Java にもあるそうで、C# と同様に呼び出し先のメソッド内で try-with-resources が使われていました。
教えていただいたうらがみさん、ありがとうございましたm(__)m

try {
    Files.readAllLines(Paths.get("test.txt"))
               .forEach(System.out::println);
}
catch (IOException e){
    System.out.println(e.getLocalizedMessage());
}

File.cs

// Decompiled with JetBrains decompiler
// Type: System.IO.File
// Assembly: mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// MVID: 65984520-5776-46EC-9044-386EC4A7B3DD
// Assembly location: /usr/lib/mono/4.5/mscorlib.dll

~省略~
public static string[] ReadAllLines(string path, Encoding encoding){
    using (StreamReader reader = new StreamReader(path, encoding))
        return File.ReadAllLines(reader);
}
~省略~

Linq で using が使えないためにこのような処理を行っているのか、このようにしているから Linq で using を使う必要がない、と判断したのかは追えていません。
(にわとりたまご的な)

また C# では IEnumerator が IDisposable を継承しているため、(必要かどうかはともかく)下記のようなことができます。

using (var n = Enumerable.Range(0, 10).GetEnumerator()){
    // 何かの処理            
}

こちらは後にも触れますが、 foreach のクリーンナップに使われるようです。

AutoCloseableとIDisposableは同じもの?

さてここまで見てきたとおり、 try-with-resources と using には共通点がかなり多いというか、ほとんど同じと言っても良さそうな気がします。

というところで気になったのが、AutoCloseable と IDisposable との違いです。
これらが対象とするクラスやその役割などは本当に同じなのでしょうか。

C♯

ドキュメントを見ると Dispose は、アンマネージ リソースに割り当てられたメモリを解放するためのものとなっています。

アンマネージ リソースというのは諸説あるようですが、 CLR が管理していないため、ガベージコレクションの対象にならないデータ、というくらいの認識で良さそう?です。

具体的にはファイル操作、HTTP通信などが該当します。

しかし、先程登場した Enumerable.Range では見た感じアンマネージ リソースは登場しないような気がします。

ということで Enumerable.Range を辿っていくと、System.Linq.Enumerable.Iterator で Disposeが定義されているようでした。

// Decompiled with JetBrains decompiler
// Type: System.Linq.Enumerable
// Assembly: System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
// MVID: FB28F097-6905-4726-B681-ECB6DF11A20B
// Assembly location: /usr/lib/mono/4.5/System.Core.dll

~省略~
      public virtual void Dispose()
      {
        this._current = default (TSource);
        this._state = -1;
      }
      ~省略~

また C# の言語仕様を見てみると、foreach などで利用される Enumerator では Dispose が呼ばれたときに以下のように扱われるようです。

Enumerator の状態と Dispose が呼ばれたときの動作

  • before(処理前): 状態を「after」にする
  • after(処理後): 何もしない
  • running(処理中): unspecified
  • suspended( yield return で宣言して実行するまでの間?): Disposeを実行して状態をデフォルトに戻す

ここから、 IDisposable 及び Dispose の役割は、アンマネージ リソースを開放するだけではなく、データのクリーンナップという範囲でもう少し広い用途に使われているように思います。

Java

ドキュメントを見ると、 try-with-resources の対象になるデータは「プログラムでの使用が終わったら閉じられなければいけないオブジェクト」「リソースとして知られるローカル変数」のようになっており、 C# より範囲が広いように感じました。

ということで結論としては、使用目的としては大体同じ、ただし詳細や Close(Dispose) の動作には異なる点がある、というくらいでしょうか。

おわりに

同じような目的で作られ、同じように使われる機能であっても、詳細を調べてみると違っているところやこれまで気づかなかった特性を見つけることができたりして楽しいものですね。

一旦この話はここで締めますが、また特筆すべき違いなどがあればその3として書こうかなと思います。

参照

try-with-resources

AutoCloseable

Stream API

IDisposable

using

アンマネージドリソース

Javaのtry-with-resourcesとC#のusingを比較する その1

はじめに

テキスト入出力で用いられる BufferedReader を始め、使用後に明示的に Close する必要があるクラスがあります。
Java7 以降から、try-with-resources 文を使うことで、使用後に自動で Close してくれるようになります。

ふ〜ん。分かるような分からないようなと思っていたのですが、ある方の発言で一気に理解ができるようになりました。

C# の using 句と同じ」

言われてみればなんで気づかなかったのか、というくらいですが。

とはいえ、本当に両者は同じなのでしょうか。

この謎を探るため急遽我々はry

両者を比較してみることにしました。

[Java]基本のクラス

確認用にこのようなクラスを用意します。

BaseSampleClass.java

public class BaseSampleClass implements AutoCloseable{
    private final String message;
    public BaseSampleClass(String message){
        this.message = message;
    }
    public String getMessage() throws Exception{
        System.out.println("getMessage " + message);
        return message;
    }
    public void close() throws Exception{
        System.out.println("closed " + message);
    }
}
  • try-with-resources を使用するためには、 AutoCloseable または Closeable を継承している必要があります。
  • 各メソッドの throws Exception は現状では不要ですが、あとでそれぞれのメソッドでエラーが出た場合の検証に使います。
  • close メソッドは今回は呼ばれたタイミングが知りたいだけであるため、ログ出力のみ行っています。

呼び出すコードはこちら。

App.java

public class App{
    public static void main( String[] args){
        System.out.println("start");

        try(BaseSampleClass class0 = new BaseSampleClass("class0")){
            System.out.println(class0.getMessage());
        }
        catch (Exception e){
            System.out.println(e.getMessage());
        }
        System.out.println("end");
    }
}

[C#]基本のクラス

C# でも同様にクラスを作っていきます。

BaseSampleClass.cs

using System;

namespace ConsoleApplication1 {
    public class BaseSampleClass: IDisposable {
        private readonly string _message;

        public BaseSampleClass(string message) {
            _message = message;
        }

        public string GetMessage() {
            Console.WriteLine("getMessage " + _message);
            return _message;
        }
        public void Dispose() {
            Console.WriteLine("disposed " + _message);
        }
    }
}
  • using 句で使用するためには、 IDisposable を継承している必要があります。

Program.cs

using System;

namespace ConsoleApplication1{
    internal class Program{
        public static void Main(string[] args){            
            Console.WriteLine("start");

            try{
                using (BaseSampleClass class0 = new BaseSampleClass("class0")){
                    Console.WriteLine(class0.GetMessage());
                }
            }
            catch (Exception e){
                Console.WriteLine(e.Message);
            }
            Console.WriteLine("end");
        }
    }
}

複数宣言

複数のインスタンスを try-with-resources で宣言するには、入れ子にすることももちろん可能ですが、下記のように書くことができます。

App.java

try(HasInnerClass class0 = new HasInnerClass();
    HasInnerClass.InnerClass class1 = class0.new InnerClass()){
      // 何か処理.
}

Java の場合はそれぞれのインスタンスの型が同じでも違っても同じように書くことができます。

しかし、 C# では若干違いがあります。

生成したいインスタンスの型が同じ場合

using (BaseSampleClass class0 = new BaseSampleClass("class0"),
    class1 = new BaseSampleClass("class1")){
      // 何か処理.
}

生成したいインスタンスの型が異なる場合

using (BaseSampleClass class0 = new BaseSampleClass("class0"))
using (HasInnerClass class1 = new HasInnerClass()){
      // 何か処理.
}

それぞれ別々に書く必要があるわけです。

また、 using の場合は if などと同じように {} 無しでも書くことができるため、上記のような書き方になります。

staticなインスタンス

staticなインスタンスの場合、 JavaC# ともに下記のように書くことができます (Java は ver.9 から)。

App.java

public class App{
    private final static BaseSampleClass S = new BaseSampleClass("final");
    public static void main( String[] args){

        try(S){
          
            // 何か処理.
        }
        catch (Exception e){
            System.out.println(e.getMessage());
        }
        System.out.println("end");
    }
}

実行順序

Java, C# ともに実行順序は下記の通りです。

  1. try (using) ブロック
  2. close() (Dispose()) メソッド
  3. catch ブロック

InnerClass

InnerClass を持っていて、両方が AutoCloseable(IDisposable) を継承している場合の順序はどのようになるでしょうか。

HasInnerClass.java

public class HasInnerClass implements AutoCloseable{
  public HasInnerClass(){
      System.out.println("constructor HasInnerClass");
  }
  public void close(){
      System.out.println("closed HasInnerClass");
  }
  public class InnerClass implements AutoCloseable {
      public InnerClass(){
          System.out.println("constructor InnerClass");
      }
      public void close(){
          System.out.println("closed InnerClass");
      }
  }
}

App.java

public class App{
    public static void main( String[] args){
      System.out.println("start");

      try(HasInnerClass class0 = new HasInnerClass();
          HasInnerClass.InnerClass class1 = class0.new InnerClass()){
            // 何か処理.
      }
      catch (Exception e) {
          System.out.println(e);
      }
      System.out.println("end");
    }
}

実行結果は以下の通りです。

start
constructor HasInnerClass
constructor InnerClass
closed InnerClass
closed HasInnerClass
end

InnerClass -> EnclosingClass の順番に close されています。

ただし、 C# の場合は InnerClass のインスタンスを生成する場合は「HasInnerClass.InnerClass class1 = new HasInnerClass.InnerClass();」となり HasInnerClass のインスタンスが不要なため、 class1 と class0 を逆に宣言できます。

この場合、EnclosingClass -> InnerClass の順に dispose(close) されます。

つまり、後から宣言されたインスタンスが先に close(dispose) されるわけですね。

例外の抑制

これも Java, C# 共通ですが、1.の try ブロックまたは 2.の close() メソッドのどちらかで例外は発生した場合、3.の catch ブロックが実行されるわけですが、 1.と2.の両方で例外が発生した場合、2.の例外は抑制されます。

BaseSampleClass.java

public class BaseSampleClass implements AutoCloseable{
    private final String message;
    public String getMessage() throws Exception{
      
        throw new Exception("exception from getMessage()");
        
    }
    public BaseSampleClass(String message){
        this.message = message;
    }
    public void close() throws Exception{
        System.out.println("closed " + message);
        
        throw new Exception("exception from close()");
        
    }
}
  • getMessage()、 close() で例外を投げるよう変更しています。

これを実行すると、以下がログに出力されます。

start
closed class0
exception from getMessage()
end

close() で投げているはずの例外が抑制されています。

抑制された例外の取得

Java では抑制された例外を取得することができます。

App.java

public class App{
    public static void main( String[] args){
        System.out.println("start");

        try(BaseSampleClass class0 = new BaseSampleClass("class0")){
            System.out.println(class0.getMessage());
        }
        catch (Exception e){
            System.out.println(e.getMessage());

            Throwable[] throwables = e.getSuppressed();
            if(throwables != null){
                for(Throwable t: throwables){
                    System.out.println(t.getMessage());
                }
            }
            
        }
        System.out.println("end");
    }
}

これを実行すると、以下がログに出力されます。

start
closed class0
exception from getMessage()
exception from close()
end

close() で投げた例外も受け取ることができていますね。

注意点としては getSuppressed() で取得できるのは「抑制された例外」のみであることです。
つまり、最初に投げた getMessage() の例外は取得できません。

そのため、 e.getMessage() と合わせて確認する必要があります。

次へ

長くなってきたので一旦切ります。

参照

try-with-resources

using

【Ubuntu】Mavenのインストール → コンパイル

はじめに

タイトル通り、UbuntuMaven をインストールしてコンパイルしたときのお話。

後述する通りインストールは特に問題ありませんでしたが、プロジェクト作成→コンパイルでエラーが発生したのでメモっておくことにします。

インストール

インストール手順に従ってインストールしていきます。

まずMaven のダウンロードページからバイナリ版をダウンロードして、任意の場所に展開します。
(今回は Documents フォルダに置きました)

http://maven.apache.org/download.cgi

.bashrc にパスを追加します。

export PATH=/home/ユーザー名/Documents/apache-maven-3.5.3/bin:$PATH
export JAVA_HOME=/home/ユーザー名/Documents/jdk-10
export PATH=$PATH:$JAVA_HOME/bin
  • 下二行は Java のパスです。 Maven で使用される Java は、この JAVA_HOME の設定を自動で見るようです。

ターミナルで「mvn -version」と打ってバージョンや使用する Java のバージョンなどの情報が出たら OK です。

プロジェクトを作成する

Getting started に従って、プロジェクトを作成します。

mvn -B archetype:generate \
  -DarchetypeGroupId=org.apache.maven.archetypes \
  -DgroupId=com.mycompany.app \
  -DartifactId=my-app
  • プロジェクトの Archetype(テンプレート) として、「org.apache.maven.archetypes」を指定しています。
  • 「BUILD SUCCESS」とメッセージがでれば OK です。

コンパイル(失敗)

じゃあ、ということで次の手順であるコンパイルをやってみます。

。。。

失敗しました/(^o^)\

[INFO] Scanning for projects...
[INFO] 
[INFO] -------------------------< com.mycompany.app:my-app >-------------------------
[INFO] Building my-app 1.0-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO] 
[INFO] --- maven-resources-plugin:2.6:resources (default-resources) @ my-app ---
[WARNING] Using platform encoding (UTF-8 actually) to copy filtered resources, i.e. build is platform dependent!
[INFO] skip non existing resourceDirectory /home/ユーザー名/Documents/workspace/my-app/src/main/resources
[INFO] 
[INFO] --- maven-compiler-plugin:3.1:compile (default-compile) @ my-app ---
[INFO] Changes detected - recompiling the module!
[WARNING] File encoding has not been set, using platform encoding UTF-8, i.e. build is platform dependent!
[INFO] Compiling 1 source file to /home/ユーザー名/Documents/workspace/my-app/target/classes
[INFO] -------------------------------------------------------------
[ERROR] COMPILATION ERROR : 
[INFO] -------------------------------------------------------------
[ERROR] Source option 5 is no longer supported. Use 6 or later.
[ERROR] Target option 1.5 is no longer supported. Use 1.6 or later.
[INFO] 2 errors 
[INFO] -------------------------------------------------------------
[INFO] ------------------------------------------------------------------------
[INFO] BUILD FAILURE
[INFO] ------------------------------------------------------------------------
[INFO] Total time: 1.286 s
[INFO] Finished at: 2018-03-27T07:12:06+09:00
[INFO] ------------------------------------------------------------------------
[ERROR] Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.1:compile (default-compile) on project my-app: Compilation failure: Compilation failure: 
[ERROR] Source option 5 is no longer supported. Use 6 or later.
[ERROR] Target option 1.5 is no longer supported. Use 1.6 or later.
[ERROR] -> [Help 1]
[ERROR] 
[ERROR] To see the full stack trace of the errors, re-run Maven with the -e switch.
[ERROR] Re-run Maven using the -X switch to enable full debug logging.
[ERROR] 
[ERROR] For more information about the errors and possible solutions, please read the following articles:
[ERROR] [Help 1] http://cwiki.apache.org/confluence/display/MAVEN/MojoFailureException

エラーの内容を見ると、何かのバージョンが合っていないようです。
また、エンコーディングの警告も出ていますね。

あれこれググった結果、「何か」とは Mavenコンパイラーのバージョンのことで、エンコーディングの指定とともに、 pom.xml(プロジェクトのフォルダ直下に生成される) に記述が必要らしいとわかりました。

Before

< 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/maven-v4_0_0.xsd">
  < modelVersion>4.0.0< /modelVersion>
  < groupId>com.mycompany.app< /groupId>
  < artifactId>my-app< /artifactId>
  < packaging>jar< /packaging>
  < version>1.0-SNAPSHOT< /version>
  < name>my-app< /name>
  < url>http://maven.apache.org< /url>
  < dependencies>
    < dependency>
      < groupId>junit< /groupId>
      < artifactId>junit< /artifactId>
      < version>3.8.1< /version>
      < scope>test< /scope>
    < /dependency>
  < /dependencies>
< /project>

After

< 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/maven-v4_0_0.xsd">
  < modelVersion>4.0.0< /modelVersion>
  < groupId>com.mycompany.app< /groupId>
  < artifactId>my-app< /artifactId>
  < packaging>jar< /packaging>
  < version>1.0-SNAPSHOT< /version>
  < name>my-app< /name>
  < url>http://maven.apache.org< /url>
  

  < properties>
    < maven.compiler.source>1.6< /maven.compiler.source>
    < maven.compiler.target>1.6< /maven.compiler.target>
    < project.build.sourceEncoding>UTF-8< /project.build.sourceEncoding>
  < /properties>
  

  < dependencies>
    < dependency>
      < groupId>junit< /groupId>
      < artifactId>junit< /artifactId>
      < version>3.8.1< /version>
      < scope>test< /scope>
    < /dependency>
  < /dependencies>
< /project>

これで「mvn compile」も正しく実行できるようになりました。

my-app > target > classes > com > mycompany > app に App.class というクラスファイルが出力されます。

実行方法は下記の2つがあるようです。

1

下記コマンドを実行する

mvn exec:java -Dexec.mainClass="mvn exec:java -Dexec.mainClass="com.mycompany.app.App""

2

Jar ファイルを作成

mvn package

Jar ファイルを実行

java -cp target/my-app-1.0-SNAPSHOT.jar com.mycompany.app.

参照

UbuntuでSDKMAN をインストールしようとして失敗したときのメモ

はじめに

相変わらず使用言語が定まりません。どうもこんにちは。

Java10 もリリースされたということで、さっそく OpenJDK10 をインストールしてみよう。SDKMAN を使って。
と思ったらエラーが出てうまくいかなかったので、対処法を探したときのメモです。

原因からするとあまり他の方の役にはたたないような気もしますが。。。

問題

下記の方法に従って SDKMAN をインストールしようとしたところ、2つ問題が発生しました。

  1. curl -s "https://get.sdkman.io" | bash」と入力しても動作せず、「curl -s get.sdkman.io | bash」だと動作した
  2. インストール完了のメッセージが出ているのに、「source "/home/ユーザー名/.sdkman/bin/sdkman-init.sh"」が見つからないとエラーが出る

2.を更に調べると、途中で zip ファイルがダウンロードできていないことがわかりました。

下記に似たような問題が発生しているようでしたが、別にどこかの IP Address をブロックしているわけでもないしなぁ。。と手が止まってしまいました。

https://github.com/sdkman/sdkman-cli/issues/536

原因

一旦 SDKMAN を諦めて、 apt-add-repository で PPA を追加しようとしたときに原因が判明しました。

「sudo add-apt-repository ppa:webupd8team/java」などを実行すると、下記のようなエラーが発生したのです。

Traceback (most recent call last):
  File "/usr/lib/python3/dist-packages/softwareproperties/ppa.py", line 358, in get_ppa_info
    ret = get_ppa_info_from_lp(user, ppa)
  File "/usr/lib/python3/dist-packages/softwareproperties/ppa.py", line 94, in get_ppa_info_from_lp
    return get_info_from_lp(lp_url)
  File "/usr/lib/python3/dist-packages/softwareproperties/ppa.py", line 88, in get_info_from_lp
    return _get_https_content_py3(lp_url)
  File "/usr/lib/python3/dist-packages/softwareproperties/ppa.py", line 112, in _get_https_content_py3
    lp_page = urllib.request.urlopen(request, cafile=LAUNCHPAD_PPA_CERT)
  File "/usr/lib/python3.6/urllib/request.py", line 213, in urlopen
    capath=capath)
  File "/usr/lib/python3.6/ssl.py", line 512, in create_default_context
    context.load_verify_locations(cafile, capath, cadata)
ssl.SSLError: unknown error (_ssl.c:3566)

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "/usr/bin/add-apt-repository", line 122, in 
    shortcut = shortcut_handler(line)
  File "/usr/lib/python3/dist-packages/softwareproperties/SoftwareProperties.py", line 864, in shortcut_handler
    ret = factory(shortcut)
  File "/usr/lib/python3/dist-packages/softwareproperties/ppa.py", line 430, in shortcut_handler
    return PPAShortcutHandler(shortcut)
  File "/usr/lib/python3/dist-packages/softwareproperties/ppa.py", line 387, in __init__
    info = get_ppa_info(self.shortcut)
  File "/usr/lib/python3/dist-packages/softwareproperties/ppa.py", line 370, in get_ppa_info
    _get_suggested_ppa_message(user, ppa))
  File "/usr/lib/python3/dist-packages/softwareproperties/ppa.py", line 327, in _get_suggested_ppa_message
    lp_user = get_info_from_lp(LAUNCHPAD_USER_API % user)
  File "/usr/lib/python3/dist-packages/softwareproperties/ppa.py", line 88, in get_info_from_lp
    return _get_https_content_py3(lp_url)
  File "/usr/lib/python3/dist-packages/softwareproperties/ppa.py", line 112, in _get_https_content_py3
    lp_page = urllib.request.urlopen(request, cafile=LAUNCHPAD_PPA_CERT)
  File "/usr/lib/python3.6/urllib/request.py", line 213, in urlopen
    capath=capath)
  File "/usr/lib/python3.6/ssl.py", line 512, in create_default_context
    context.load_verify_locations(cafile, capath, cadata)
ssl.SSLError: unknown error (_ssl.c:3566)

おや?

どうも、 SDKMAN をインストールする前に「sudo add-apt-repository --remove ppa:webupd8team/java」で追加していた PPA を削除していたのですが、これがまずかったようです。

上記エラーでググったときに出てきた、「ca-certificates」はインストール済みでしたが、再インストールしたことでエラーがでなくなりました。

sudo apt install ca-certificates

PPA 追加と合わせて SDKMAN も問題なくインストールできるようになりました。

あ〜よかったよかった。

参照