CodeGym /Java Blogu /Rastgele /Yansıma API'si: Yansıma. Java'nın karanlık yüzü
John Squirrels
Seviye
San Francisco

Yansıma API'si: Yansıma. Java'nın karanlık yüzü

grupta yayınlandı
Selamlar, genç Padawan. Bu yazıda size, Java programcılarının yalnızca imkansız gibi görünen durumlarda kullandıkları bir güç olan Kuvvet'ten bahsedeceğim. Java'nın karanlık yüzü, Reflection API'sidir. Java'da yansıma, Java Reflection API kullanılarak gerçekleştirilir.

Java yansıması nedir?

İnternette kısa, doğru ve popüler bir tanım var. Yansıma ( geç Latin refleksio'dan - geri dönmek için ), çalışırken bir program hakkındaki verileri keşfetme mekanizmasıdır. Yansıma, alanlar, yöntemler ve sınıf kurucuları hakkındaki bilgileri keşfetmenizi sağlar. Yansıma, derleme zamanında mevcut olmayan ancak çalıştırma sırasında kullanılabilir hale gelen türlerle çalışmanıza olanak tanır. Hata bilgisi vermek için yansıtma ve mantıksal olarak tutarlı bir model, doğru dinamik kodun oluşturulmasını mümkün kılar. Başka bir deyişle, Java'da yansımanın nasıl çalıştığını anlamak size bir dizi harika fırsat sunacaktır. Kelimenin tam anlamıyla sınıfları ve bileşenlerini dengeleyebilirsiniz. İşte yansımanın izin verdiği temel bir liste:
  • Bir nesnenin sınıfını öğrenin/belirleyin;
  • Bir sınıfın değiştiricileri, alanları, yöntemleri, sabitleri, yapıcıları ve üst sınıfları hakkında bilgi edinin;
  • Uygulanan arabirim(ler)e hangi yöntemlerin ait olduğunu öğrenin;
  • Çalışma zamanına kadar sınıf adı bilinmeyen bir sınıf örneği oluşturun;
  • Bir nesnenin alanlarının değerlerini ada göre alın ve ayarlayın;
  • Bir nesnenin yöntemini ada göre çağırın.
Yansıma neredeyse tüm modern Java teknolojilerinde kullanılır. Bir platform olarak Java'nın üzerinde düşünmeden bu kadar yaygın bir şekilde benimsenmiş olabileceğini hayal etmek zor. Büyük ihtimalle olmazdı. Artık genel olarak teorik bir kavram olarak yansımaya aşina olduğunuza göre, pratik uygulamasına geçelim! Reflection API'nin tüm yöntemlerini öğrenmeyeceğiz, yalnızca pratikte gerçekten karşılaşacağınız yöntemleri öğreneceğiz. Yansıtma, sınıflarla çalışmayı içerdiğinden, adında basit bir sınıfla başlayacağız MyClass:

public class MyClass {
   private int number;
   private String name = "default";
//    public MyClass(int number, String name) {
//        this.number = number;
//        this.name = name;
//    }
   public int getNumber() {
       return number;
   }
   public void setNumber(int number) {
       this.number = number;
   }
   public void setName(String name) {
       this.name = name;
   }
   private void printData(){
       System.out.println(number + name);
   }
}
Gördüğünüz gibi, bu çok temel bir sınıftır. Parametreleri olan yapıcı kasıtlı olarak yorumlanır. Buna daha sonra geri döneceğiz. Sınıfın içeriğine dikkatlice baktıysanız, muhtemelen ad alanı için bir alıcının olmadığını fark etmişsinizdir . Ad alanının kendisi özel erişim değiştiricisiyle işaretlenmiştir : ona sınıfın dışından erişemeyiz, bu da değerini geri alamayacağımız anlamına gelir . " Öyleyse sorun ne ?" diyorsun. "Bir alıcı ekleyin veya erişim değiştiricisini değiştirin". Ve haklısın, eğerMyClassderlenmiş bir AAR kitaplığında veya değişiklik yapma yeteneği olmayan başka bir özel modüldeydi. Uygulamada, bu her zaman olur. Ve dikkatsiz bir programcı basitçe bir alıcı yazmayı unutmuş . Yansımayı hatırlamanın tam zamanı! Sınıfın özel isim alanına ulaşmaya çalışalım MyClass:

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; // No getter =(
   System.out.println(number + name); // Output: 0null
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(number + name); // Output: 0default
}
Biraz önce ne olduğunu analiz edelim. Java'da, adında harika bir sınıf var Class. Yürütülebilir bir Java uygulamasındaki sınıfları ve arabirimleri temsil eder. Bu makalenin konusu bu olmadığı için Classve arasındaki ilişkiyi ele almayacağız . ClassLoaderArdından, bu sınıfın alanlarını almak için yöntemi çağırmanız gerekir getFields(). Bu yöntem, bu sınıfın tüm erişilebilir alanlarını döndürür. Bu bizim için işe yaramıyor, çünkü alanımız private , bu yüzden yöntemi kullanıyoruz getDeclaredFields(). Bu yöntem aynı zamanda bir dizi sınıf alanı döndürür, ancak artık özel ve korumalı alanları da içermektedir . Bu durumda ilgilendiğimiz alanın adını biliyoruz, böylece yöntemi kullanabiliriz getDeclaredField(String), buradaStringistenen alanın adıdır. Not: getFields()ve getDeclaredFields()bir üst sınıfın alanlarını döndürmeyin! Harika. AdımızaField atıfta bulunan bir nesnemiz var . Alan public olmadığından , onunla çalışmak için erişim izni vermeliyiz. Yöntem daha fazla ilerlememizi sağlar. Artık isim alanı tamamen bizim kontrolümüz altında! Değerini, sınıfımızın bir örneği olan nesnenin yöntemini çağırarak alabilirsiniz . Türü dönüştürüyoruz ve değeri name değişkenimize atıyoruz. Ad alanına yeni bir değer ayarlamak için bir ayarlayıcı bulamazsak , set yöntemini kullanabilirsiniz : setAccessible(true)Fieldget(Object)ObjectMyClassString

field.set(myClass, (String) "new value");
Tebrikler! Yansımanın temellerinde ustalaştınız ve özel bir alana eriştiniz! try/catchBloğa ve işlenen istisna türlerine dikkat edin . IDE size onların varlığının kendi başına gerekli olduğunu söyleyecektir, ancak adlarından neden burada olduklarını açıkça anlayabilirsiniz. Hareketli! Fark etmiş olabileceğiniz gibi, sınıfımızda MyClasszaten sınıf verileri hakkında bilgi görüntülemek için bir yöntem var:

private void printData(){
       System.out.println(number + name);
   }
Ama bu programcı burada da parmak izlerini bırakmış. Yöntemin özel bir erişim değiştiricisi vardır ve verileri her seferinde görüntülemek için kendi kodumuzu yazmamız gerekir. Ne dağınıklık. Yansımamız nereye gitti? Aşağıdaki işlevi yazın:

public static void printData(Object myClass){
   try {
       Method method = myClass.getClass().getDeclaredMethod("printData");
       method.setAccessible(true);
       method.invoke(myClass);
   } catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException e) {
       e.printStackTrace();
   }
}
Buradaki prosedür, bir alanı almak için kullanılanla hemen hemen aynıdır. İstenen yönteme adıyla erişir ve ona erişim izni veririz. MethodVe yöntem dediğimiz nesnede , invoke(Object, Args)burada Objectda sınıfın bir örneğidir MyClass. Argsyöntemin argümanlarıdır, ancak bizimkinde yoktur. printDataŞimdi bilgiyi görüntülemek için işlevi kullanıyoruz :

public static void main(String[] args) {
   MyClass myClass = new MyClass();
   int number = myClass.getNumber();
   String name = null; //?
   printData(myClass); // Output: 0default
   try {
       Field field = myClass.getClass().getDeclaredField("name");
       field.setAccessible(true);
       field.set(myClass, (String) "new value");
       name = (String) field.get(myClass);
   } catch (NoSuchFieldException | IllegalAccessException e) {
       e.printStackTrace();
   }
   printData(myClass);// Output: 0new value
}
Yaşasın! Artık sınıfın özel yöntemine erişimimiz var. Ancak, yöntemin argümanları varsa ve neden yapıcı yorumlanırsa? Her şey kendi zamanında. Başlangıçtaki tanımdan, yansımanın çalışma zamanında (program çalışırken) bir sınıfın örneklerini oluşturmanıza izin verdiği açıktır! Sınıfın tam adını kullanarak bir nesne oluşturabiliriz. Sınıfın tam adı, paketinin yolu da dahil olmak üzere sınıf adıdır .
Yansıma API'si: Yansıma.  Java'nın karanlık yüzü - 2
Paket hiyerarşimde , Sınıfım'ın tam adı "reflection.MyClass" olacaktır. Bir sınıfın adını öğrenmenin de basit bir yolu var (sınıfın adını bir String olarak döndür):

MyClass.class.getName()
Sınıfın bir örneğini oluşturmak için Java yansımasını kullanalım:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       myClass = (MyClass) clazz.newInstance();
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
       e.printStackTrace();
   }
   System.out.println(myClass); // Output: created object reflection.MyClass@60e53b93
}
Bir Java uygulaması başladığında, tüm sınıflar JVM'ye yüklenmez. Kodunuz sınıfa atıfta bulunmuyorsa MyClass, ClassLoadersınıfları JVM'ye yüklemekten sorumlu olan , sınıfı asla yüklemez. Bu, onu yüklemeye zorlamanız ClassLoaderve bir değişken biçiminde bir sınıf açıklaması almanız gerektiği anlamına gelir Class. Bu nedenle forName(String), Stringaçıklamasına ihtiyacımız olan sınıfın adı olan bir yöntemimiz var. Nesneyi aldıktan sonra Сlass, yöntemin çağrılması, bu açıklama kullanılarak oluşturulan newInstance()bir nesneyi döndürür . ObjectGeriye kalan tek şey bu nesneyi bizim için tedarik etmek.MyClasssınıf. Serin! Bu zordu ama anlaşılırdı umarım. Artık kelimenin tam anlamıyla tek bir satırda bir sınıf örneği oluşturabiliriz! Ne yazık ki, açıklanan yaklaşım yalnızca varsayılan oluşturucuyla (parametreler olmadan) çalışacaktır. Yöntemleri ve yapıcıları parametrelerle nasıl çağırırsınız? Yapıcımızın açıklamasını kaldırmanın zamanı geldi. Beklendiği gibi, newInstance()varsayılan kurucu bulunamıyor ve artık çalışmıyor. Sınıf örneklemesini yeniden yazalım:

public static void main(String[] args) {
   MyClass myClass = null;
   try {
       Class clazz = Class.forName(MyClass.class.getName());
       Class[] params = {int.class, String.class};
       myClass = (MyClass) clazz.getConstructor(params).newInstance(1, "default2");
   } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
       e.printStackTrace();
   }
   System.out.println(myClass);// Output: created object reflection.MyClass@60e53b93
}
Yöntem getConstructors(), sınıf oluşturucuları elde etmek için sınıf tanımında çağrılmalı ve ardından getParameterTypes()bir oluşturucunun parametrelerini almak için çağrılmalıdır:

Constructor[] constructors = clazz.getConstructors();
for (Constructor constructor : constructors) {
   Class[] paramTypes = constructor.getParameterTypes();
   for (Class paramType : paramTypes) {
       System.out.print(paramType.getName() + " ");
   }
   System.out.println();
}
Bu bize tüm yapıcıları ve parametrelerini alır. Örneğimde, önceden bilinen belirli parametrelere sahip belirli bir kurucuya atıfta bulunuyorum. newInstanceVe bu yapıcıyı çağırmak için, bu parametrelerin değerlerini ilettiğimiz yöntemi kullanırız . invokeYöntemleri çağırmak için kullanırken aynı olacaktır . Bu şu soruyu akla getiriyor: Oluşturucuları yansıma yoluyla çağırmak ne zaman işe yarar? Başta da belirtildiği gibi, modern Java teknolojileri Java Reflection API olmadan yapamaz. Örneğin, popüler Darer'ı oluşturmak için ek açıklamaları yöntemlerin ve yapıcıların yansımasıyla birleştiren Dependency Injection (DI).Android geliştirme için kütüphane. Bu makaleyi okuduktan sonra, kendinizi güvenle Java Reflection API yöntemleri konusunda eğitimli olarak düşünebilirsiniz. Yansımayı boşuna Java'nın karanlık tarafı olarak adlandırmazlar. OOP paradigmasını tamamen kırar. Java'da kapsülleme, başkalarının belirli program bileşenlerine erişimini gizler ve kısıtlar. Özel değiştiriciyi kullandığımızda, o alana yalnızca bulunduğu sınıfın içinden erişilmesini amaçlıyoruz. Ve programın sonraki mimarisini bu prensibe göre inşa ediyoruz. Bu yazıda, herhangi bir yere gitmek için yansımayı nasıl kullanabileceğinizi gördük. Yaratıcı tasarım deseni Singletonmimari çözüm olarak buna güzel bir örnektir. Temel fikir, bu modeli uygulayan bir sınıfın, tüm programın yürütülmesi sırasında yalnızca bir örneğe sahip olacağıdır. Bu, özel erişim değiştiricisini varsayılan kurucuya ekleyerek gerçekleştirilir. Ve bir programcının bu tür sınıfların daha fazla örneğini oluşturmak için yansımayı kullanması çok kötü olurdu. Bu arada, yakın zamanda bir iş arkadaşımın çok ilginç bir soru sorduğunu duydum: Singleton modelini uygulayan bir sınıf miras alınabilir mi? Bu durumda, yansıma bile güçsüz olabilir mi? Makale hakkındaki görüşlerinizi ve cevabınızı aşağıdaki yorumlara bırakın ve kendi sorularınızı orada sorun!

Daha fazla okuma:

Yorumlar
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION