C++時代にCスタイルのキャスト演算子の悪さについて教えられたので、最初にJava 5 Java.lang.Class
がcast
メソッドを取得したことを発見しました。
最終的に、キャストを処理するOO方法があると思いました。
Class.cast
は、C++のstatic_cast
とは異なります。 reinterpret_cast
に似ています。予想される場所でコンパイルエラーが生成されることはなく、代わりにランタイムになります。さまざまな動作を示す簡単なテストケースを次に示します。
package test;
import static org.junit.Assert.assertTrue;
import org.junit.Test;
public class TestCast
{
static final class Foo
{
}
static class Bar
{
}
static final class BarSubclass
extends Bar
{
}
@Test
public void test ( )
{
final Foo foo = new Foo( );
final Bar bar = new Bar( );
final BarSubclass bar_subclass = new BarSubclass( );
{
final Bar bar_ref = bar;
}
{
// Compilation error
final Bar bar_ref = foo;
}
{
// Compilation error
final Bar bar_ref = (Bar) foo;
}
try
{
// !!! Compiles fine, runtime exception
Bar.class.cast( foo );
}
catch ( final ClassCastException ex )
{
assertTrue( true );
}
{
final Bar bar_ref = bar_subclass;
}
try
{
// Compiles fine, runtime exception, equivalent of C++ dynamic_cast
final BarSubclass bar_subclass_ref = (BarSubclass) bar;
}
catch ( final ClassCastException ex )
{
assertTrue( true );
}
}
}
だから、これらは私の質問です。
Class.cast()
はジェネリックの土地に追放されるべきですか?そこでは、正当な用途がかなりあります。Class.cast()
が使用され、コンパイル時に違法な条件を判別できる場合、コンパイラはコンパイルエラーを生成する必要がありますか?「generics land」での警告を避けるためにClass.cast(Object)
を使用したことがあります。私は頻繁にこのようなことをしているメソッドを見ます:
@SuppressWarnings("unchecked")
<T> T doSomething() {
Object o;
// snip
return (T) o;
}
多くの場合、次のものに置き換えるのが最善です。
<T> T doSomething(Class<T> cls) {
Object o;
// snip
return cls.cast(o);
}
これが、Class.cast(Object)
の唯一のユースケースです。
コンパイラの警告について:Class.cast(Object)
はコンパイラにとって特別なものではないと思います。静的に使用すると最適化できます(つまり、Foo.class.cast(o)
ではなくcls.cast(o)
)が、それを使用している人は誰もいません。
まず、ほとんどすべてのキャストを行うことを強くお勧めしません。可能な限り制限する必要があります。 Javaのコンパイル時の厳密に型指定された機能の利点を失います。
いずれにしても、Class.cast()
は、主にリフレクションを介してClass
トークンを取得するときに使用する必要があります。書くのがより慣用的です
MyObject myObject = (MyObject) object
のではなく
MyObject myObject = MyObject.class.cast(object)
編集:コンパイル時のエラー
全体的に、Javaは実行時にのみキャストチェックを実行します。ただし、そのようなキャストが決して成功しないことを証明できる場合、コンパイラーはエラーを発行できます(たとえば、スーパータイプではない別のクラスにクラスをキャストし、そのタイプ階層にないクラス/インターフェースに最終クラスタイプをキャストします)。ここで、Foo
とBar
は互いに階層にないクラスであるため、キャストは成功しません。
言語間でコンストラクトや概念を翻訳しようとすると、常に問題が発生し、しばしば誤解を招きます。キャストも例外ではありません。特にJavaは動的言語であり、C++は多少異なるためです。
Javaでのキャストは、どのように実行しても、実行時に行われます。型情報は実行時に保持されます。 C++はもう少し複雑です。 C++の構造体を別の構造体にキャストできます。これは、それらの構造体を表すバイトの再解釈にすぎません。 Javaはそのようには機能しません。
JavaとC++のジェネリックも大きく異なります。 JavaでC++をどのように行うかについて、過度に心配する必要はありません。 Java方法で物事を行う方法を学ぶ必要があります。
Class.cast()
がJavaコードで使用されることはほとんどありません。それが使用される場合、通常は実行時にのみ既知の型で(つまり、それぞれのClass
オブジェクトを介して、またはいくつかの型パラメーターによって)使用されます。これは、ジェネリックを使用するコードでのみ非常に役立ちます(これは、以前に導入されなかった理由でもあります)。
notreinterpret_cast
に似ています。これはnotで、通常のキャストよりも実行時に型システムを壊すことができるためです(つまり、 " 「ブレーク」汎用型パラメーターが、「実際の」型を「ブレーク」することはできません)。
通常、Cスタイルのキャスト演算子の悪さはJavaには当てはまりません。 Cスタイルのキャストのように見えるJavaコードは、参照タイプがJavaのdynamic_cast<>()
に最も似ています(Javaにはランタイムタイプ情報があります)。
通常、C++キャスト演算子とJavaキャストを比較するのはかなり困難です。Javaでは参照のみキャストでき、オブジェクトへの変換は行われないためです(プリミティブ値のみこの構文を使用して変換できます)。
C++とJavaは異なる言語です。
Java Cスタイルのキャスト演算子は、C/C++バージョンよりもはるかに制限されています。 Javaキャストは、実行中のオブジェクトを新しいクラスにキャストできない場合(またはコンパイル時にコードに十分な情報がある場合)の例外として、C++ dynamic_castのようになります。したがって、C型キャストを使用しないというC++の考え方は、Javaでは良い考えではありません。
一般に、キャスト演算子はより簡潔であり、コンパイラーによって分析されてコードに関する露骨な問題を吐き出すことができるため、Class#castメソッドよりも優先されます。
Class#castは、コンパイル時ではなく、実行時の型チェックを行います。
Class#castには、特にリフレクション操作に関しては、確かにユースケースがあります。
ラムダはJavaに来たので、たとえば抽象型を扱う場合、コレクション/ストリームAPIでClass#castを使用するのが個人的に好きです。
Dog findMyDog(String name, Breed breed) {
return lostAnimals.stream()
.filter(Dog.class::isInstance)
.map(Dog.class::cast)
.filter(dog -> dog.getName().equalsIgnoreCase(name))
.filter(dog -> dog.getBreed() == breed)
.findFirst()
.orElse(null);
}
個人的には、これを使用してJSONからPOJOへのコンバーターを作成したことがあります。関数で処理されたJSONObjectに配列またはネストされたJSONObjectsが含まれる場合(ここのデータがプリミティブ型またはString
でないことを意味します)、class.cast()
を使用してsetterメソッドを呼び出そうとしますこの方法で:
public static Object convertResponse(Class<?> clazz, JSONObject readResultObject) {
...
for(Method m : clazz.getMethods()) {
if(!m.isAnnotationPresent(convertResultIgnore.class) &&
m.getName().toLowerCase().startsWith("set")) {
...
m.invoke(returnObject, m.getParameters()[0].getClass().cast(convertResponse(m.getParameters()[0].getType(), readResultObject.getJSONObject(key))));
}
...
}
これが非常に役立つかどうかはわかりませんが、前述のとおり、リフレクションは、class.cast()
の非常に少数の正当なユースケースの1つです。少なくとももう1つの例があります。
最も言及されているasいキャスト警告を削除することに加えて、Class.castは一般的なキャストで主に使用されるランタイムキャストであり、一般的な情報は実行時に消去され、各ジェネリックはObjectと見なされるため、初期のClassCastExceptionをスローします。
たとえば、serviceLoderはオブジェクトを作成するときにこのトリックを使用します。Sp = service.cast(c.newInstance()); S P =(S)c.newInstance();の場合、これはクラスキャスト例外をスローします。警告を表示しない場合があります'タイプセーフティ:オブジェクトからSへの未チェックのキャスト'。(Object P =(Object)c.newInstance();と同じ)
-単純に、キャストされたオブジェクトがキャストクラスのインスタンスであることを確認し、キャスト演算子を使用して警告を非表示にしてキャストおよび非表示にします。
動的キャストのJava実装:
@SuppressWarnings("unchecked")
public T cast(Object obj) {
if (obj != null && !isInstance(obj))
throw new ClassCastException(cannotCastMsg(obj));
return (T) obj;
}
private S nextService() {
if (!hasNextService())
throw new NoSuchElementException();
String cn = nextName;
nextName = null;
Class<?> c = null;
try {
c = Class.forName(cn, false, loader);
} catch (ClassNotFoundException x) {
fail(service,
"Provider " + cn + " not found");
}
if (!service.isAssignableFrom(c)) {
fail(service,
"Provider " + cn + " not a subtype");
}
try {
S p = service.cast(c.newInstance());
providers.put(cn, p);
return p;
} catch (Throwable x) {
fail(service,
"Provider " + cn + " could not be instantiated",
x);
}
throw new Error(); // This cannot happen
}