vaguely

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

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