@blog.justoneplanet.info

日々勉強

ネストしたクラスとインターフェース

ネストした型はエンクロージング型にアクセスできる場合にだけアクセスできる。また、アクセスの観点などから1レベルだけのネストが推奨される。

■staticのネストしたクラス

public class BankAccount {
    private long number;
    private long balance;
    public static class Permissions {
        public boolean canDeposit, canWithdraw, canClose;
    }
}

■ネストしたインターフェース

ネストしたインターフェースは常にstaticであるが慣習的に省略される。

■内部クラス

staticでないクラスメンバーはそのクラスのインスタンスと関連付けされている。内部クラスのオブジェクトは常にエンクロージングインスタンスと関連付けされている。

public class BankAccount {
    private long number;
    private long balance;
    private Action lastAction;
    public class Action {
        private String action;
        private long amount;
        Action(String action, long amount) {
            this.action = action;
            this.amount = amount;
        }
        public String toString() {
            return number + ": " + action + " " + amount;
        }
    }
    public void deposit(long amount) {
        balance += amount;
        lastAction = new Action("deposit", amount);
    }
    public void withdraw(long amount) {
        balance -= amount;
        lastAction = new Action("withdraw", amount);
    }
}

通常、内部クラスのオブジェクトはdepositやwithdrawの中で行われたようにエンクロージングクラスのインスタンスメソッドの内部で生成される。

lastAction = new Action("deposit", amount);

上述のコードを厳密に書くと以下のようになる。

lastAction = this.new Action("deposit", amount);

制限

staticフィールドを持つことができないのでstatic finalにするかインスタンスフィールドにする必要がある。

アクセス

以下のようにエンクロージングクラスは明示的な参照を通して、内部クラスのprivateメンバにアクセスできる。

public void deposit(long amount) {
    balance += amount;
    lastAction = new Action("deposit", amount);
    String str = lastAction.action;
}

以下のように内部クラスからエンクロージングクラスにアクセスすることができる。

public String toString() {
    return number + ": " + action + " " + amount;
}

また以下のように限定的なthisを用いて記述することもできる。

public String toString() {
    return BankAccount.this.number + ": " + action + " " + amount;
}

但し、staticメソッドなどから限定的なthisを用いることはできない。

■拡張

以下のようなクラスを考える。

class Outer {
    class Inner {}
}

以下のようにして拡張することができる。

class ExOuter extends Outer {
    class ExInner extends Inner {}
}

内部サブクラスのエンクロージングクラスがOuterのサブクラスでない場合、内部サブクラス自身が内部クラスでない場合、superを通してInnerのコンストラクタを呼び出す際にOuterオブジェクトへの明示的な参照を提供しなくてはならない。

class Unrelated extends Outer.Inner {
    Unrelated(Outer ref) {
        ref.super();
    }
}

■隠蔽

以下のように同名のメソッドを内部クラスで定義すると、引数に関係なくエンクロージングクラスのメソッドが隠蔽される。

class Outer {
    void print() {}
    void print(int val) {}
    class Inner {
        void print() {}// エンクロージングクラスの全てのprintメソッドを隠蔽する
        void show() {
            print(1);// コンパイルエラー
        }
    }
}

■ローカル内部クラス

以下のようにメソッド本体(コンストラクタや初期化ブロックなど)のコードブロックの中で内部クラスを定義することができる。

public void onClick(View v) {
    class BankAccount {}
    BankAccount bankAccount = new BankAccount();
}

コードブロックの外からはアクセスできず完全にローカルである。また、staticとする事はできない。

■無名内部クラス

以下のようにして無名内部クラスを定義することができる。無名内部クラスはコンストラクタを持つことができない。

private static Iterator<Object> walk(final Object[] objs) {
    return new Iterator<Object>() {
        private int position = 0;
        @Override
        public boolean hasNext() {
            return (position < objs.length);
        }
        @Override
        public Object next() {
            if (position >= objs.length) {
                throw new NoSuchElementException();
            }
            return objs[position++];
        }
        @Override
        public void remove() {
            throw new UnsupportedOperationException();
        }
    };
}

明示的なコンストラクタを必要とする場合はローカル内部クラスを使用するべきである。また、6行を超えるような無名内部クラスは可読性の観点から避けるべきである。

■ネストした型の継承

以下のようなコードを考える。

abstract class Device {
    abstract class Port {

    }
}
class Printer extends Device {
    class SerialPort extends Port {

    }
    Port serial = new SerialPort();
}
class HighSpeedPrinter extends Printer {
    class SerialPort extends Printer.SerialPort {

    }
    // 継承されたメンバserialは親クラスのSerialPortのインスタンスである
}

一方で以下のようにファクトリーメソッドを用いてオブジェクトを生成する場合を考える。

class Printer extends Device {
    class SerialPort extends Port {

    }
    Port serial = createSerialPort();
    protected Port createSerialPort() {
        return new SerialPort();
    }
}
class HighSpeedPrinter extends Printer {
    class EnhancedSerialPort extends SerialPort {

    }
    // 継承されたメンバserialは初期化の際にカレントクラスでOverrideされたcreateSerialPortが呼ばれる
    protected Port createSerialPort() {
        return new EnhancedSerialPort();
    }
}

■インターフェース内でのネスト

以下のようにしてインターフェース内でネストしたクラスを定義することができる。

interface Changeable {
    class Record {
        public Object changer;
        public String changeDesc;
    }
    Record getLastChange();
}

暗黙的にpublicかつstaticになる。

インターフェース内での変更可能な変数

以下のようにすることでSharedDataを利用しているコードは、data参照を通して共通の状態を共有できる。

interface SharedData {
    class Data {// 暗黙的にstatic
        private int x = 0;
        public int getX() {return x;}
        public void setX(int x){this.x = x;}
    }
    Data data = new Data();
}

Javaのクラスとオブジェクト

■クラス

メンバ

フィールド
クラスやそのオブジェクトに付属するデータ変数で状態保持などに使用
メソッド
オブジェクトの振る舞いを定義
ネストしたクラス(インターフェース)
入れ子になっているクラスやインターフェースの宣言
class Body {

    // フィールド
    public long number;

    // メソッド
    public void hello(String str) {
        
    }

    // ネストしたクラス
    public class {
        
    }
}

■フィールド

以下のようにしてフィールドを初期化することができる。

class Body {
    double zero  = 0.0;
    double sum   = 1.1 + 2.2;
    double zero2 = zero;
    double four  = Math.sqrt(2);
    Dog pochi    = new Dog();
}

但し、例外を投げるような処理を右辺に書くことはできない。フィールドが初期化されなかった場合、型に応じたデフォルトの初期値が代入される。

boolean false
char ‘\u0000′
byte, short, int, long 0
float, double +0.0
オブジェクト参照 null

個人的には明示しておいた方が読みやすいと思う。

staticフィールド

実体が1つだけのフィールドを実現する。クラス変数とも呼ばれる。

class Dog {
    long nextID = 0;// コンストラクタで生成ごとに+するとか
}

finalフィールド

初期化後に「不変」であるフィールド。

class Dog {
    int numOfLegs = 4;// 変わらないよ
}
  • オブジェクトの不変な属性であるか
  • オブジェクトが生成されたときに決まっているか
  • オブジェクトが生成されたときに任意の値を設定するのは実用的かつ適切か

■アクセス制御

private
クラス自身からのみアクセス可能
修飾子無し
同一パッケージ内からアクセス可能
protected
クラス自身とサブクラスからアクセス可能
public
メンバーが定義されているクラスにアクセスできるクラスからアクセス可能

privateとprotected

メンバのみに指定可能。メンバ以外のクラスやインターフェースには指定不可。

publicとprotected

制御が及ばないコードによって自身の実装の変更が不可能になる可能性がある。

パッケージアクセスとprivate

実装の一部であり外部のクラスから隠蔽される。

パッケージ

同一パッケージのクラスは関連しているべきである。

■オブジェクトの生成と初期化

// オブジェクトを参照する変数の宣言で生成はしてない
Dog shiro;
// 生成を伴う
Dog pochi = new Dog();

コンストラクタ

newによってオブジェクトが返される前に実行される。

class Dog {
    private String mName;
    Dog(String name) {
        mName = name;
    }
}
  • 引数をとることができる
  • クラス名と同名である
  • アクセス修飾可能である
  • クラスのメンバではない

コンストラクタは複数定義することが可能であり、以下のように自身のコンストラクタをinvokeすることもできる。

class Dog {
    private String mName;
    Dog() {
        // 任意の処理
    }
    Dog(String name) {
        this();
        mName = name;
    }
}

publicでないコンストラクタはオブジェクトを生成できるクラスを制限できる。

メモ

  • 生成時に必要な領域を割り当てて領域を初期化し新しいオブジェクトへの参照を返す
  • 生成時に必要な空きメモリを確保できない場合はOutOfMemoryError例外を投げる
  • 生成にはnewを用いるが明示的に削除はせず、GCを使用してメモリ管理する
  • オブジェクトが不要になった場合、そのオブジェクトを参照しないようにしなければならない
  • メソッド内のローカル変数ならばメソッドの処理が終わるだけで参照されなくなる
  • フィールドのような場合はnullを代入することで参照されなくなる

■初期化ブロック

以下のようにすることでコンストラクタよりも前に複雑な処理を行うことができる。

class Dog {
    private String mName;
    {
        // 任意の処理
    }
    Dog(String name) {
        mName = name;
    }
    public String cry() {
        return "bow";
    }
}

コンストラクタを持つことができない無名内部クラスを書く場合に有用である。

  • 複数のブロックを記述することができる
  • 例外aを投げることができる(全てのコンストラクタがaの例外を投げると宣言されているときに限る)

初期化ブロックをstaticで修飾することもできる。


■メソッド

staticメソッド

クラスメソッドのこと。staticメソッドからはstaticでないメンバにはアクセスできない。

class Util {
    private String str1 = "sample1";// helloメソッドからアクセス不可能
    private static String str2 = "sample2";// helloメソッドからアクセス可能
    public static String hello() {
        return "Hello!"
    }
}

メソッドの呼び出し

メソッドはドット演算子を使用し、参照を通してオブジェクトに対する操作として呼び出される。

reference.method(arguments);

インスタンスメソッドの呼び出しは以下のようになる。

Dog pochi = new Dog("Pochi");
pochi.cry();

staticメソッド(クラスメソッド)の呼び出しは以下のようになる。

String hello = Util.hello();
toStringメソッド

オブジェクトの状態を表す文字列表現を返すメソッド。

class Dog {
    private String mName;
    {
        // 任意の処理
    }
    Dog(String name) {
        mName = name;
    }
    public String cry() {
        return "bow";
    }
    @Override
    public String toString() {
        return "Dog{name : " + mName + "}";
    }
}

可変長引数を持つメソッド

メソッドの最後の引数は、特定の型のシーケンスであることを宣言できる。

class Mail {
    public static void send(String... to) {
        for(int i = 0; i < to.length; i++)
    }
}

メソッドの実行と戻り値

以下の条件の時に、メソッドの処理が終了する。

return文が実行される
一つの宣言された戻り値型に代入可能な値を返すか、例外を投げる必要がある
メソッドの終わりに処理が達する
戻り値型はvoidとなる
catchできない例外が投げられる
throwsを記述する必要がある

引き数の値

class PassRef {
    public static void main(String[] args) {
        Dog pochi = new Dog("Pochi");
        System.out.println(pochi.getName());
        commonName(pochi);
        System.out.println(pochi.getName());
        pochi = null;// おまけ
    }
    public static void commonName(Dog dogRef) {
        dogRef.name = "Shiro";
        dogRef = null;
    }
}
/*
Pochi
Shiro
*/

pochiと(commonName内の)dogRefは同じ実体のオブジェクトを参照しているので、nameを変更すると実体のオブジェクトのnameフィールドが変更される。

一方で、(commonName内の)dogRefは参照をコピーしたものであるため、nullを代入しても実体がnullにはならない。

ちなみにpochiにnullを代入すると実体を参照している変数がなくなるので、GCの対象となる。

  • 引き数は全て値渡しであり、オブジェクトは常に参照である
  • Javaに参照渡しというものは存在しない
  • 引き数の宣言時にfinalを指定して不変にできる

■this

フィールドの識別子が引き数と同名であることなどにより隠蔽されている時、カレントオブジェクトへの参照として使用する。

class Dog {
    private String name;
    Dog(String name) {
        this.name = name;
    }
    public String cry() {
        return this.name + " : bow";
    }
}

■メソッドのオーバーロード

メソッドはシグネチャ(名前とパラメータの数とパラメータの型)を持つ。従って、同名のメソッドを複数持つことが可能であり、このことをオーバーロードという。(オーバーライドとは違う)

class Dog {
    private String name;
    private int age;
    Dog(String name) {
        this.name = name;
    }
    Dog(String name, int age) {
        this.name = name;
        this.age = age;
    }
}

メソッドでなくコンストラクタでも同様である。但し、可変長引数を持つときは注意する必要がある。

■staticメンバー名のインポート

static double tanh(double x) {
    return (Math.exp(x) - Math.exp(-x)) / (Math.exp(x) + Math.exp(-x));
}

以下のようなstatic import文を用いることによって

import static java.lang.Math.exp

以下のように簡略化して可読性の高いコードを記述することができる。

static double tanh(double x) {
    return (exp(x) - exp(-x)) / (exp(x) + exp(-x));
}

オンデマンドstatic import文

コンパイラが知らない名前が見つかった場合、指定された型が特定の名前のstaticメンバを持つかどうか調べる。

import static java.lang.Math.*

タイプの手間を省くために使用するのではなく、可読性と明瞭性を改善する目的で使用する。

■mainメソッド

プログラムを起動する場合に実行される最初のメソッドでpublic static void main(String[] args)でなければならない。

class Echo {
    public static void mail(String[] args) {
        for(int i = 0; i < args.length; i++){
            System.out.println(args[i]);
        }
    }
}

以下のコマンドで実行することができる。

java Echo hoge fuga
#hoge
#fuga

個々のクラスはmainメソッドを持って構わないので、アプリケーションは複数のmainメソッドを持つことができる。

■nativeメソッド

Javaで書かれていない既存のコードを使用するプログラムを書く必要がある場合やハードウェアを直接操作する必要がある場合、JavaからCやC++で記述されたメソッドを呼び出すことができる。

public native int getCPUID();

ネイティブメソッドはfinal, static, synchronized, public, protected, privateで修飾することができるが、abstract, strictfpで修飾することはできない。