Saya ingin dapat menulis kelas Java dalam satu paket yang dapat mengakses metode non-publik dari kelas dalam paket lain tanpa harus membuatnya menjadi subclass dari kelas lain. Apakah ini mungkin?
Saya ingin dapat menulis kelas Java dalam satu paket yang dapat mengakses metode non-publik dari kelas dalam paket lain tanpa harus membuatnya menjadi subclass dari kelas lain. Apakah ini mungkin?
Jawaban:
Berikut ini adalah trik kecil yang saya gunakan di JAWA untuk mereplikasi mekanisme teman C ++.
Katakanlah saya punya kelas Romeo
dan kelas lain Juliet
. Mereka berada dalam paket yang berbeda (keluarga) karena alasan kebencian.
Romeo
ingin cuddle
Juliet
dan Juliet
ingin hanya membiarkannya Romeo
cuddle
.
Dalam C ++, Juliet
akan menyatakan Romeo
sebagai (kekasih) friend
tetapi tidak ada hal seperti itu di java.
Inilah kelas dan triknya:
Wanita pertama:
package capulet;
import montague.Romeo;
public class Juliet {
public static void cuddle(Romeo.Love love) {
Objects.requireNonNull(love);
System.out.println("O Romeo, Romeo, wherefore art thou Romeo?");
}
}
Jadi metode Juliet.cuddle
adalah public
tetapi Anda perlu Romeo.Love
untuk menyebutnya. Ini menggunakan ini Romeo.Love
sebagai "tanda tangan keamanan" untuk memastikan bahwa hanya Romeo
dapat memanggil metode ini dan memeriksa bahwa cinta itu nyata sehingga runtime akan membuang NullPointerException
jika itu null
.
Sekarang anak laki-laki:
package montague;
import capulet.Juliet;
public class Romeo {
public static final class Love { private Love() {} }
private static final Love love = new Love();
public static void cuddleJuliet() {
Juliet.cuddle(love);
}
}
Kelas Romeo.Love
bersifat publik, tetapi konstruktornya adalah publik private
. Karena itu siapa pun dapat melihatnya, tetapi hanya Romeo
dapat membuatnya. Saya menggunakan referensi statis sehingga Romeo.Love
yang tidak pernah digunakan hanya dibangun sekali dan tidak berdampak optimasi.
Oleh karena itu, Romeo
bisa cuddle
Juliet
dan hanya dia bisa karena hanya dia bisa membangun dan akses sebuah Romeo.Love
contoh, yang dibutuhkan oleh Juliet
untuk cuddle
dia (atau dia akan menampar Anda dengan NullPointerException
).
Romeo
's Love
untuk Julia
kekal dengan mengubah love
bidang menjadi final
;-).
Para perancang Java secara eksplisit menolak gagasan tentang pertemanan karena ia bekerja di C ++. Anda memasukkan "teman" Anda dalam paket yang sama. Keamanan pribadi, yang dilindungi, dan dikemas diberlakukan sebagai bagian dari desain bahasa.
James Gosling ingin Java menjadi C ++ tanpa kesalahan. Saya percaya dia merasa teman itu kesalahan karena melanggar prinsip OOP. Paket memberikan cara yang masuk akal untuk mengatur komponen tanpa terlalu murni tentang OOP.
NR menunjukkan bahwa Anda bisa curang menggunakan refleksi, tetapi bahkan itu hanya berfungsi jika Anda tidak menggunakan SecurityManager. Jika Anda mengaktifkan keamanan standar Java, Anda tidak akan bisa menipu dengan refleksi kecuali Anda menulis kebijakan keamanan untuk mengizinkannya secara khusus.
friend
melanggar OOP (khususnya, lebih dari akses paket tidak) maka dia benar-benar tidak memahaminya (sangat mungkin, banyak orang salah paham).
Konsep 'teman' berguna di Jawa, misalnya, untuk memisahkan API dari implementasinya. Adalah umum untuk kelas implementasi membutuhkan akses ke internal kelas API tetapi ini tidak boleh diekspos ke klien API. Ini dapat dicapai dengan menggunakan pola 'Friend Accessor' sebagaimana dirinci di bawah ini:
Kelas diekspos melalui API:
package api;
public final class Exposed {
static {
// Declare classes in the implementation package as 'friends'
Accessor.setInstance(new AccessorImpl());
}
// Only accessible by 'friend' classes.
Exposed() {
}
// Only accessible by 'friend' classes.
void sayHello() {
System.out.println("Hello");
}
static final class AccessorImpl extends Accessor {
protected Exposed createExposed() {
return new Exposed();
}
protected void sayHello(Exposed exposed) {
exposed.sayHello();
}
}
}
Kelas yang menyediakan fungsionalitas 'teman':
package impl;
public abstract class Accessor {
private static Accessor instance;
static Accessor getInstance() {
Accessor a = instance;
if (a != null) {
return a;
}
return createInstance();
}
private static Accessor createInstance() {
try {
Class.forName(Exposed.class.getName(), true,
Exposed.class.getClassLoader());
} catch (ClassNotFoundException e) {
throw new IllegalStateException(e);
}
return instance;
}
public static void setInstance(Accessor accessor) {
if (instance != null) {
throw new IllegalStateException(
"Accessor instance already set");
}
instance = accessor;
}
protected abstract Exposed createExposed();
protected abstract void sayHello(Exposed exposed);
}
Contoh akses dari kelas dalam paket implementasi 'teman':
package impl;
public final class FriendlyAccessExample {
public static void main(String[] args) {
Accessor accessor = Accessor.getInstance();
Exposed exposed = accessor.createExposed();
accessor.sayHello(exposed);
}
}
Ada dua solusi untuk pertanyaan Anda yang tidak melibatkan menjaga semua kelas dalam paket yang sama.
Yang pertama adalah menggunakan Friend Accessor / Paket Friend yang dijelaskan dalam (Desain API Praktis, Tulach 2008).
Yang kedua adalah menggunakan OSGi. Ada artikel di sini menjelaskan bagaimana OSGi menyelesaikan ini.
Sejauh yang saya tahu, itu tidak mungkin.
Mungkin, Anda bisa memberi kami beberapa detail tentang desain Anda. Pertanyaan seperti ini kemungkinan merupakan hasil dari kekurangan desain.
Pertimbangkan saja
Jawaban eirikma mudah dan luar biasa. Saya mungkin menambahkan satu hal lagi: alih-alih memiliki metode yang dapat diakses publik, getFriend () untuk mendapatkan teman yang tidak dapat digunakan, Anda dapat melangkah lebih jauh dan melarang mendapatkan teman tanpa token: getFriend (Service.FriendToken). FriendToken ini akan menjadi kelas publik dalam dengan konstruktor pribadi, sehingga hanya Layanan yang dapat membuat instantiate.
Berikut adalah contoh kasus penggunaan yang jelas dengan Friend
kelas yang dapat digunakan kembali . Manfaat dari mekanisme ini adalah kesederhanaan penggunaan. Mungkin bagus untuk memberi kelas unit tes akses yang lebih banyak daripada aplikasi lainnya.
Untuk memulai, berikut adalah contoh cara menggunakan Friend
kelas.
public class Owner {
private final String member = "value";
public String getMember(final Friend friend) {
// Make sure only a friend is accepted.
friend.is(Other.class);
return member;
}
}
Kemudian dalam paket lain Anda dapat melakukan ini:
public class Other {
private final Friend friend = new Friend(this);
public void test() {
String s = new Owner().getMember(friend);
System.out.println(s);
}
}
The Friend
kelas adalah sebagai berikut.
public final class Friend {
private final Class as;
public Friend(final Object is) {
as = is.getClass();
}
public void is(final Class c) {
if (c == as)
return;
throw new ClassCastException(String.format("%s is not an expected friend.", as.getName()));
}
public void is(final Class... classes) {
for (final Class c : classes)
if (c == as)
return;
is((Class)null);
}
}
Namun, masalahnya adalah ia bisa disalahgunakan seperti:
public class Abuser {
public void doBadThings() {
Friend badFriend = new Friend(new Other());
String s = new Owner().getMember(badFriend);
System.out.println(s);
}
}
Sekarang, mungkin benar bahwa Other
kelas tidak memiliki konstruktor publik, oleh karena itu membuat Abuser
kode di atas tidak mungkin. Namun, jika kelas Anda memang memiliki konstruktor publik maka mungkin disarankan untuk menduplikasi kelas Teman sebagai kelas dalam. Ambil Other2
kelas ini sebagai contoh:
public class Other2 {
private final Friend friend = new Friend();
public final class Friend {
private Friend() {}
public void check() {}
}
public void test() {
String s = new Owner2().getMember(friend);
System.out.println(s);
}
}
Dan kemudian Owner2
kelas akan seperti ini:
public class Owner2 {
private final String member = "value";
public String getMember(final Other2.Friend friend) {
friend.check();
return member;
}
}
Perhatikan bahwa Other2.Friend
kelas memiliki konstruktor pribadi, sehingga menjadikan ini cara yang jauh lebih aman untuk melakukannya.
Solusi yang diberikan mungkin bukan yang paling sederhana. Pendekatan lain didasarkan pada ide yang sama seperti di C ++: anggota pribadi tidak dapat diakses di luar paket / ruang lingkup pribadi, kecuali untuk kelas tertentu yang pemiliknya punya teman sendiri.
Kelas yang membutuhkan akses teman ke anggota harus membuat "kelas teman" abstrak publik publik di mana kelas yang memiliki properti tersembunyi dapat mengekspor akses ke, dengan mengembalikan subclass yang menerapkan metode implementasi akses. Metode "API" kelas teman bisa bersifat pribadi sehingga tidak dapat diakses di luar kelas yang membutuhkan akses teman. Satu-satunya pernyataan adalah panggilan ke anggota yang dilindungi abstrak yang mengimplementasikan kelas ekspor.
Berikut kodenya:
Pertama tes yang memverifikasi bahwa ini benar-benar berfungsi:
package application;
import application.entity.Entity;
import application.service.Service;
import junit.framework.TestCase;
public class EntityFriendTest extends TestCase {
public void testFriendsAreOkay() {
Entity entity = new Entity();
Service service = new Service();
assertNull("entity should not be processed yet", entity.getPublicData());
service.processEntity(entity);
assertNotNull("entity should be processed now", entity.getPublicData());
}
}
Kemudian Layanan yang membutuhkan akses teman ke paket pribadi anggota Entity:
package application.service;
import application.entity.Entity;
public class Service {
public void processEntity(Entity entity) {
String value = entity.getFriend().getEntityPackagePrivateData();
entity.setPublicData(value);
}
/**
* Class that Entity explicitly can expose private aspects to subclasses of.
* Public, so the class itself is visible in Entity's package.
*/
public static abstract class EntityFriend {
/**
* Access method: private not visible (a.k.a 'friendly') outside enclosing class.
*/
private String getEntityPackagePrivateData() {
return getEntityPackagePrivateDataImpl();
}
/** contribute access to private member by implementing this */
protected abstract String getEntityPackagePrivateDataImpl();
}
}
Akhirnya: kelas Entity yang menyediakan akses yang ramah ke anggota pribadi paket hanya ke class application.service.Service.
package application.entity;
import application.service.Service;
public class Entity {
private String publicData;
private String packagePrivateData = "secret";
public String getPublicData() {
return publicData;
}
public void setPublicData(String publicData) {
this.publicData = publicData;
}
String getPackagePrivateData() {
return packagePrivateData;
}
/** provide access to proteced method for Service'e helper class */
public Service.EntityFriend getFriend() {
return new Service.EntityFriend() {
protected String getEntityPackagePrivateDataImpl() {
return getPackagePrivateData();
}
};
}
}
Oke, saya harus akui itu sedikit lebih lama dari "layanan teman :: Layanan;" tetapi dimungkinkan untuk mempersingkat saat mempertahankan waktu kompilasi dengan menggunakan anotasi.
Di Jawa dimungkinkan untuk memiliki "persahabatan terkait paket". Ini bisa bermanfaat untuk pengujian unit. Jika Anda tidak menentukan pribadi / publik / dilindungi di depan metode, itu akan menjadi "teman dalam paket". Kelas dalam paket yang sama akan dapat mengaksesnya, tetapi akan menjadi pribadi di luar kelas.
Aturan ini tidak selalu diketahui, dan merupakan perkiraan yang baik untuk kata kunci "teman" C ++. Saya merasa ini pengganti yang baik.
Saya pikir bahwa kelas teman di C ++ seperti konsep kelas dalam di Jawa. Menggunakan kelas-dalam Anda sebenarnya dapat mendefinisikan kelas tertutup dan kelas tertutup. Kelas tertutup memiliki akses penuh ke anggota publik dan pribadi dari kelas terlampir itu. lihat tautan berikut: http://docs.oracle.com/javase/tutorial/java/javaOO/nested.html
Saya pikir, pendekatan menggunakan pola pengaksesan teman terlalu rumit. Saya harus menghadapi masalah yang sama dan saya menyelesaikan menggunakan copy constructor yang baik dan lama, yang dikenal dari C ++, di Java:
public class ProtectedContainer {
protected String iwantAccess;
protected ProtectedContainer() {
super();
iwantAccess = "Default string";
}
protected ProtectedContainer(ProtectedContainer other) {
super();
this.iwantAccess = other.iwantAccess;
}
public int calcSquare(int x) {
iwantAccess = "calculated square";
return x * x;
}
}
Dalam aplikasi Anda, Anda dapat menulis kode berikut:
public class MyApp {
private static class ProtectedAccessor extends ProtectedContainer {
protected ProtectedAccessor() {
super();
}
protected PrivateAccessor(ProtectedContainer prot) {
super(prot);
}
public String exposeProtected() {
return iwantAccess;
}
}
}
Keuntungan dari metode ini adalah hanya aplikasi Anda yang memiliki akses ke data yang dilindungi. Ini bukan pengganti kata kunci teman. Tapi saya pikir itu sangat cocok ketika Anda menulis perpustakaan khusus dan Anda perlu mengakses data yang dilindungi.
Setiap kali Anda harus berurusan dengan instance ProtectedContainer Anda dapat membungkus ProtectedAccessor Anda di sekitarnya dan Anda mendapatkan akses.
Ini juga bekerja dengan metode yang dilindungi. Anda mendefinisikannya terlindungi di API Anda. Kemudian dalam aplikasi Anda, Anda menulis kelas pembungkus pribadi dan mengekspos metode yang dilindungi sebagai publik. Itu dia.
ProtectedContainer
dapat disubklasifikasikan di luar paket!
Jika Anda ingin mengakses metode yang dilindungi, Anda bisa membuat subkelas dari kelas yang ingin Anda gunakan yang memperlihatkan metode yang ingin Anda gunakan sebagai publik (atau internal ke namespace menjadi lebih aman), dan memiliki instance kelas itu di kelas Anda (gunakan sebagai proxy).
Sejauh menyangkut metode pribadi (saya pikir) Anda kurang beruntung.
Saya setuju bahwa dalam kebanyakan kasus kata kunci teman tidak perlu.
Dan akhirnya, jika benar-benar diperlukan, ada pola pengakses teman yang disebutkan dalam jawaban lain.
Tidak menggunakan kata kunci atau lebih.
Anda dapat "menipu" menggunakan refleksi dll, tetapi saya tidak akan merekomendasikan "curang".
Metode yang saya temukan untuk menyelesaikan masalah ini adalah membuat objek accessor, seperti:
class Foo {
private String locked;
/* Anyone can get locked. */
public String getLocked() { return locked; }
/* This is the accessor. Anyone with a reference to this has special access. */
public class FooAccessor {
private FooAccessor (){};
public void setLocked(String locked) { Foo.this.locked = locked; }
}
private FooAccessor accessor;
/** You get an accessor by calling this method. This method can only
* be called once, so calling is like claiming ownership of the accessor. */
public FooAccessor getAccessor() {
if (accessor != null)
throw new IllegalStateException("Cannot return accessor more than once!");
return accessor = new FooAccessor();
}
}
Kode pertama yang memanggil getAccessor()
"klaim kepemilikan" dari accessor. Biasanya, ini adalah kode yang menciptakan objek.
Foo bar = new Foo(); //This object is safe to share.
FooAccessor barAccessor = bar.getAccessor(); //This one is not.
Ini juga memiliki keunggulan dibandingkan mekanisme teman C ++, karena memungkinkan Anda membatasi akses pada level per-instance , dibandingkan dengan level per-kelas . Dengan mengontrol referensi accessor, Anda mengontrol akses ke objek. Anda juga dapat membuat banyak pengakses, dan memberikan akses yang berbeda untuk masing-masingnya, yang memungkinkan kontrol yang baik atas kode apa yang dapat mengakses apa:
class Foo {
private String secret;
private String locked;
/* Anyone can get locked. */
public String getLocked() { return locked; }
/* Normal accessor. Can write to locked, but not read secret. */
public class FooAccessor {
private FooAccessor (){};
public void setLocked(String locked) { Foo.this.locked = locked; }
}
private FooAccessor accessor;
public FooAccessor getAccessor() {
if (accessor != null)
throw new IllegalStateException("Cannot return accessor more than once!");
return accessor = new FooAccessor();
}
/* Super accessor. Allows access to secret. */
public class FooSuperAccessor {
private FooSuperAccessor (){};
public String getSecret() { return Foo.this.secret; }
}
private FooSuperAccessor superAccessor;
public FooSuperAccessor getAccessor() {
if (superAccessor != null)
throw new IllegalStateException("Cannot return accessor more than once!");
return superAccessor = new FooSuperAccessor();
}
}
Akhirnya, jika Anda ingin sesuatu menjadi sedikit lebih terorganisir, Anda dapat membuat objek referensi, yang menyatukan semuanya. Ini memungkinkan Anda untuk mengklaim semua pengakses dengan satu pemanggilan metode, serta menyatukannya dengan instance tertautnya. Setelah Anda memiliki referensi, Anda dapat membagikan aksesor ke kode yang membutuhkannya:
class Foo {
private String secret;
private String locked;
public String getLocked() { return locked; }
public class FooAccessor {
private FooAccessor (){};
public void setLocked(String locked) { Foo.this.locked = locked; }
}
public class FooSuperAccessor {
private FooSuperAccessor (){};
public String getSecret() { return Foo.this.secret; }
}
public class FooReference {
public final Foo foo;
public final FooAccessor accessor;
public final FooSuperAccessor superAccessor;
private FooReference() {
this.foo = Foo.this;
this.accessor = new FooAccessor();
this.superAccessor = new FooSuperAccessor();
}
}
private FooReference reference;
/* Beware, anyone with this object has *all* the accessors! */
public FooReference getReference() {
if (reference != null)
throw new IllegalStateException("Cannot return reference more than once!");
return reference = new FooReference();
}
}
Setelah banyak memukul-mukul (bukan jenis yang baik), ini adalah solusi terakhir saya, dan saya sangat menyukainya. Ini fleksibel, mudah digunakan, dan memungkinkan kontrol yang sangat baik atas akses kelas. ( Akses dengan referensi saja sangat berguna.) Jika Anda menggunakan protected sebagai ganti private untuk accessor / referensi, subkelas Foo bahkan dapat mengembalikan referensi yang diperluas dari getReference
. Itu juga tidak memerlukan refleksi apa pun, sehingga dapat digunakan di lingkungan apa pun.
Saya lebih suka delegasi atau komposisi atau kelas pabrik (tergantung pada masalah yang menyebabkan masalah ini) untuk menghindari menjadikannya kelas publik.
Jika itu adalah masalah "antarmuka / implementasi kelas dalam paket yang berbeda", maka saya akan menggunakan kelas pabrik umum yang akan dalam paket yang sama dengan paket impl dan mencegah pemaparan dari kelas impl.
Jika itu adalah masalah "Saya benci membuat kelas / metode ini hanya untuk menyediakan fungsionalitas ini untuk beberapa kelas lain dalam paket yang berbeda", maka saya akan menggunakan kelas delegasi publik dalam paket yang sama dan hanya mengekspos bagian fungsionalitas itu saja. dibutuhkan oleh kelas "orang luar".
Beberapa keputusan ini didorong oleh arsitektur classloading server target (bundel OSGi, WAR / EAR, dll.), Penerapan dan konvensi penamaan paket. Misalnya, solusi yang diajukan di atas, pola 'Friend Accessor' pintar untuk aplikasi java normal. Saya ingin tahu apakah akan sulit untuk mengimplementasikannya di OSGi karena perbedaan gaya classloading.
Saya tidak tahu apakah itu berguna bagi siapa pun tetapi saya menanganinya dengan cara berikut:
Saya membuat antarmuka (AdminRights).
Setiap kelas yang seharusnya dapat memanggil fungsi tersebut harus mengimplementasikan AdminRights.
Kemudian saya membuat fungsi HasAdminRights sebagai berikut:
private static final boolean HasAdminRights()
{
// Gets the current hierarchy of callers
StackTraceElement[] Callers = new Throwable().getStackTrace();
// Should never occur with me but if there are less then three StackTraceElements we can't check
if (Callers.length < 3)
{
EE.InvalidCode("Couldn't check for administrator rights");
return false;
} else try
{
// Now we check the third element as this function is the first and the function wanting to check for the rights the second. We try to use it as a subclass of AdminRights.
Class.forName(Callers[2].getClassName()).asSubclass(AdminRights.class);
// If everything worked up to now, it has admin rights!
return true;
} catch (java.lang.ClassCastException | ClassNotFoundException e)
{
// In the catch, something went wrong and we can deduce that the caller has no admin rights
EE.InvalidCode(Callers[1].getClassName() + " doesn't have administrator rights");
return false;
}
}