「«アクセス修飾子»についてお話します。以前にも一度お話しましたが、反復は学習の柱です。」

他のクラスがクラスのメソッドおよび変数に対して持つアクセス (可視性) を制御できます。アクセス修飾子は、「誰がこのメソッド/変数にアクセスできますか?」という質問に答えます。各メソッドまたは変数に対して指定できる修飾子は 1 つだけです。

1) « public » 修飾子。

public修飾子でマークされた変数、メソッド、またはクラスには、プログラム内のどこからでもアクセスできます。これは最高度のオープン性であり、制限はありません。

2) «プライベート»修飾子。

private修飾子でマークされた変数、メソッド、またはクラスは、それが宣言されているクラス内でのみアクセスできます。マークされたメソッドまたは変数は、他のすべてのクラスから隠されます。これは最高度のプライバシーであり、自分のクラスだけがアクセスできます。このようなメソッドは継承されず、オーバーライドできません。さらに、子孫クラスではアクセスできません。

3)  «デフォルトの修飾子»。

変数またはメソッドが修飾子でマークされていない場合、「デフォルト」修飾子でマークされているとみなされます。この修飾子を持つ変数とメソッドは、それらが宣言されているパッケージ内のすべてのクラス、およびそれらのクラスにのみ表示されます。この修飾子は「package」または「package private」アクセスとも呼ばれ、変数およびメソッドへのアクセスがクラスを含むパッケージ全体に公開されていることを示しています。

4) « protected » 修飾子。

このアクセス レベルは、packageよりもわずかに広範です。protected修飾子でマークされた変数、メソッド、またはクラスは、そのパッケージ (「パッケージ」など) およびすべての継承クラスからアクセスできます。

この表ですべてを説明します。

可視性のタイプ キーワード アクセス
あなたのクラス あなたのパッケージ 子孫 すべてのクラス
プライベート プライベート はい いいえ いいえ いいえ
パッケージ (修飾子なし) はい はい いいえ いいえ
保護されています 保護された はい はい はい いいえ
公共 公共 はい はい はい はい

この表を簡単に覚える方法があります。あなたが遺言書を書いていると想像してください。すべてのものを 4 つのカテゴリに分類しています。あなたの物を誰が使うのですか?

誰がアクセスできるのか 修飾子
だけ  プライベート 個人的な日記
家族 (修飾子なし) 家族写真
家族と相続人 保護された 家族の財産
みんな 公共 紀要

「同じパッケージ内のクラスが 1 つのファミリーの一部であると想像するのとよく似ています。」

「また、メソッドのオーバーライドに関する興味深いニュアンスもいくつかお伝えしたいと思います。」

1) 抽象メソッドの暗黙的な実装。

次のコードがあるとします。

コード
class Cat
{
 public String getName()
 {
  return "Oscar";
 }
}

そして、このクラスを継承する Tiger クラスを作成し、新しいクラスにインターフェイスを追加することにしました。

コード
class Cat
{
 public String getName()
 {
   return "Oscar";
 }
}
interface HasName
{
 String getName();
 int getWeight();
}
class Tiger extends Cat implements HasName
{
 public int getWeight()
 {
  return 115;
 }

}

IntelliJ IDEA が実装するように指示した不足しているメソッドをすべて実装するだけだと、後でバグの検索に長い時間を費やすことになる可能性があります。

Tiger クラスには Cat から継承された getName メソッドがあり、これは HasName インターフェイスの getName メソッドの実装として解釈されることがわかります。

「それについて恐ろしいことは何もありません。」

「それほど悪くはありませんが、ミスが入り込みやすい場所です。」

しかし、さらに悪いことになる可能性があります。

コード
interface HasWeight
{
 int getValue();
}
interface HasSize
{
 int getValue();
}
class Tiger extends Cat implements HasWeight, HasSize
{
 public int getValue()
 {
  return 115;
 }
}

複数のインターフェイスから常に継承できるわけではないことがわかりました。より正確に言えば、継承することはできますが、正しく実装することはできません。例を見てください。どちらのインターフェースでも getValue() メソッドを実装する必要がありますが、重さなのかサイズなのか、何を返すべきかは明確ではありません。これに対処しなければならないのは非常に不快なことです。

「同意します。メソッドを実装したいのですが、実装できません。すでに基本クラスから同じ名前のメソッドを継承しています。壊れています。」

「しかし、良いニュースがあります。」

2)可視性の拡大。型を継承すると、メソッドの可視性を拡張できます。見た目はこんな感じです。

Javaコード 説明
class Cat
{
 protected String getName()
 {
  return "Oscar";
 }
}
class Tiger extends Cat
{
 public String getName()
 {
  return "Oscar Tiggerman";
 }
}
メソッドの可視性を から に拡張しましprotectedpublic
コード なぜこれが「合法」なのか
public static void main(String[] args)
{
 Cat cat = new Cat();
 cat.getName();
}
すべてが素晴らしいです。ここでは、可視性が子孫クラスで拡張されたことさえ知りません。
public static void main(String[] args)
{
 Tiger tiger = new Tiger();
 tiger.getName();
}
ここでは、可視性が拡張されたメソッドを呼び出します。

これが不可能な場合でも、Tiger でいつでもメソッドを宣言できます。
public String getPublicName()
{
super.getName(); //保護されたメソッドを呼び出します
}

言い換えれば、私たちはセキュリティ違反について話しているのではありません。

public static void main(String[] args)
{
 Cat catTiger = new Tiger();
 catTiger.getName();
}
基本クラス ( Cat )のメソッドを呼び出すために必要な条件がすべて満たされている場合、子孫の型 ( Tiger ) のメソッドを呼び出すためにそれらの条件は確実に満たされます。メソッド呼び出しの制限は強力ではなく、弱かったからです。

「完全に理解したかどうかはわかりませんが、これは可能だということは覚えておきます。」

3) 戻り値の型を絞り込む。

オーバーライドされたメソッドでは、戻り値の型を縮小された参照型に変更できます。

Javaコード 説明
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Tiger getMyParent()
 {
  return (Tiger) this.parent;
 }
}
メソッドをオーバーライドしgetMyParent、オブジェクトを返すようになりましたTiger
コード なぜこれが「合法」なのか
public static void main(String[] args)
{
 Cat parent = new Cat();

 Cat me = new Cat();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
すべてが素晴らしいです。ここでは、getMyParent メソッドの戻り値の型が子孫クラスで拡張されていることさえわかりません。

「古いコード」がどのように機能し、機能するか。

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Tiger me = new Tiger();
 me.setMyParent(parent);
 Tiger myParent = me.getMyParent();
}
ここでは戻り値の型が絞り込まれたメソッドを呼び出します。

これが不可能な場合でも、いつでも Tiger でメソッドを宣言できます。
public Tiger getMyTigerParent()
{
return (Tiger) this.parent;
}

言い換えれば、セキュリティ違反や型キャスト違反はありません。

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 me.setMyParent(parent);
 Cat myParent = me.getMyParent();
}
変数の型を基本クラス (Cat) に拡張しましたが、ここではすべてが正常に機能します。

オーバーライドにより、正しい setMyParent メソッドが呼び出されます。

また、 getMyParent メソッドを呼び出すときに心配することはありません。戻り値は Tiger クラスのものであっても、問題なく基本クラス (Cat)myParent 変数に割り当てることができるからです。

Tiger オブジェクトは、Tiger 変数と Cat 変数の両方に安全に格納できます。

「はい、わかりました。メソッドをオーバーライドするとき、基本クラスのみを処理でき、クラスについて何も知らないコードにオブジェクトを渡す場合、これがどのように機能するかを認識する必要があります。

「そのとおりです。では、大きな疑問は、なぜメソッドをオーバーライドするときに戻り値の型を絞り込むことができないのかということです。」

「この場合、基本クラスのコードが動作しなくなるのは明らかです。」

Javaコード 問題の説明
class Cat
{
 public Cat parent;
 public Cat getMyParent()
 {
  return this.parent;
 }
 public void setMyParent(Cat cat)
 {
  this.parent = cat;
 }
}
class Tiger extends Cat
{
 public Object getMyParent()
 {
  if (this.parent != null)
   return this.parent;
  else
   return "I'm an orphan";
 }
}
getMyParent メソッドをオーバーロードし、戻り値の型を絞り込みました。

ここではすべてが順調です。

public static void main(String[] args)
{
 Tiger parent = new Tiger();

 Cat me = new Tiger();
 Cat myParent = me.getMyParent();
}
その後、このコードは機能しなくなります。

getMyParent メソッドは実際には Tiger オブジェクトに対して呼び出されるため、オブジェクトの任意のインスタンスを返すことができます。

そして、課題の前にチェックはありません。したがって、Cat 型の myParent 変数に String 参照が格納される可能性は十分にあります。

「素晴らしい例ですね、アミーゴ!」

Java では、メソッドが呼び出される前に、オブジェクトにそのようなメソッドがあるかどうかはチェックされません。すべてのチェックは実行時に行われます。そして、欠落しているメソッドへの [仮説的な] 呼び出しにより、プログラムは存在しないバイトコードを実行しようとする可能性が高くなります。これは最終的に致命的なエラーにつながり、オペレーティング システムがプログラムを強制的に終了します。

「ああ、もうわかったよ。」