



Не использовать ключевое слово или около того.
Вы можете «обмануть», используя отражение и т.д., но я бы не рекомендовал «обмануть».
Насколько я знаю, это невозможно.
Возможно, Вы расскажете подробнее о своем дизайне. Подобные вопросы, скорее всего, являются результатом недостатков дизайна.
Просто подумай
Если вы хотите получить доступ к защищенным методам, вы можете создать подкласс класса, который вы хотите использовать, который предоставляет методы, которые вы хотите использовать как общедоступные (или внутренние по отношению к пространству имен, чтобы быть безопаснее), и иметь экземпляр этого класса в своем классе. (используйте его как прокси).
Что касается частных методов (я думаю), вам не повезло.
Разработчики Java явно отвергли идею друга, поскольку она работает на C++. Вы кладете своих «друзей» в один пакет. Частная, защищенная и пакетная безопасность обеспечивается как часть языкового дизайна.
Джеймс Гослинг хотел, чтобы Java была C++ без ошибок. Я полагаю, он считал, что этот друг был ошибкой, потому что это нарушает принципы ООП. Пакеты обеспечивают разумный способ организации компонентов без излишней строгости в отношении ООП.
NR указал, что вы можете обмануть, используя отражение, но даже это работает, только если вы не используете SecurityManager. Если вы включите стандартную безопасность Java, вы не сможете обмануть с помощью отражения, если не напишете политику безопасности, специально разрешающую это.
Я не хочу быть педантом, но модификаторы доступа не являются механизмом безопасности.
Модификаторы доступа являются частью модели безопасности Java. Я специально имел в виду java.lang.RuntimePermission для отражения: accessDeclaredMembers и accessClassInPackage.
Если Гослинг действительно думал, что friend нарушает ООП (в частности, больше, чем доступ к пакету), то он действительно не понимал этого (вполне возможно, многие люди это неправильно понимают).
Компоненты класса иногда необходимо разделить (например, реализация и API, основной объект и адаптер). Защита на уровне пакета в то же время слишком разрешительна и слишком ограничительна, чтобы делать это должным образом.
@GregD Они мог считаются механизмом безопасности в том смысле, что они помогают предотвратить неправильное использование разработчиками члена класса. Думаю, их лучше называть предохранительный механизм.
Мне просто пришлось проголосовать за написание и творческое решение. Однако я надеюсь, что мне никогда не придется делать такие вещи в продакшене.
Однажды я видел решение, основанное на отражении, которое выполняло «проверку друзей» во время выполнения с использованием отражения и проверки стека вызовов, чтобы узнать, разрешено ли это делать классу, вызывающему метод. Будучи проверкой во время выполнения, она имеет очевидный недостаток.
Концепция «друга» полезна в Java, например, для отделения API от его реализации. Обычно классам реализации требуется доступ к внутренним компонентам классов API, но они не должны открываться для клиентов API. Это может быть достигнуто с помощью шаблона «Friend Accessor», как подробно описано ниже:
Класс, предоставляемый через 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();
}
}
}
Класс, обеспечивающий функциональность «друга»:
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);
}
Пример доступа из класса в пакете реализации friend:
package impl;
public final class FriendlyAccessExample {
public static void main(String[] args) {
Accessor accessor = Accessor.getInstance();
Exposed exposed = accessor.createExposed();
accessor.sayHello(exposed);
}
}
Потому что я не знал, что означает «статика» в классе «Exposed»: статический блок - это блок оператора внутри класса Java, который будет выполняться, когда класс впервые загружается в JVM. Подробнее на javatutorialhub.com/…
Интересный шаблон, но он требует, чтобы классы Exposed и Accessor были общедоступными, в то время как классы, реализующие API (т. Е. Набор классов Java, реализующих набор общедоступных интерфейсов Java), лучше были бы «защищены по умолчанию» и, таким образом, были бы недоступны для клиента. чтобы отделить типы от их реализаций.
Я изрядно устарел на своей Яве, так что простите за невежество. В чем преимущество этого решения "Ромео и Джульетта", размещенного Salomon BRYS? Эта реализация напугала бы меня, если бы я наткнулся на нее в базе кода (без вашего объяснения, т. Е. Тяжелых комментариев). Подход Ромео и Джульетты очень прост для понимания.
Такой подход сделает проблемы видимыми только во время выполнения, в то время как неправильное использование Ромео и Джульетты сделает их видимыми во время компиляции во время разработки.
@ymajoros Пример «Ромео и Джульетта» не делает видимым злоупотребление во время компиляции. Он полагается на то, что аргумент передается правильно и генерируется исключение. Это оба действия во время выполнения.
Я думаю, что этот пример можно было бы немного упростить, сделав Accessor общедоступным статическим вложенным классом Enclosed с частным конструктором и общедоступными методами. Затем просто внедрите экземпляры Accessor друзьям, как вы уже делаете. Классы в других пакетах могли бы использовать аксессор, только если он был внедрен. Я не думаю, что есть большая выгода от помещения суперкласса доступа в пакет friend, потому что вы должны сделать методы защищенными, что означает, что они в любом случае видны внешнему миру, например. через JavaDocs.
@Radiodef, похоже, вопрос касается способа узнать о злоупотреблениях во время компиляции (что-то вроде оператора «друга»). Этот пример действительно касается проверок во время выполнения, поэтому он не совсем подходит.
@ymajoros Я не знаю, при чем тут то, что я сказал. Однако этот ответ действительно обеспечивает проверки во время компиляции, в отличие от примера «Ромео и Джульетта». В этом примере методы Exposed не видны другим пакетам, а методы средства доступа защищены, поэтому они доступны только для пакета друзей. На самом деле это проверки во время компиляции.
Я предпочитаю делегирование, композицию или фабричный класс (в зависимости от проблемы, которая приводит к этой проблеме), чтобы не делать его публичным классом.
Если это проблема «классы интерфейса / реализации в разных пакетах», то я бы использовал общедоступный фабричный класс, который находился бы в том же пакете, что и пакет impl, и предотвратил бы раскрытие класса impl.
Если это проблема типа «я ненавижу делать этот класс / метод общедоступным только для того, чтобы предоставить эту функциональность для некоторого другого класса в другом пакете», то я бы использовал публичный класс делегата в том же пакете и раскрыл только эту часть функциональности. нужен классу "аутсайдеров".
Некоторые из этих решений определяются архитектурой загрузки классов целевого сервера (пакет OSGi, WAR / EAR и т. д.), Соглашениями о развертывании и именовании пакетов. Например, предложенное выше решение, шаблон «Friend Accessor», подходит для обычных Java-приложений. Интересно, сложно ли реализовать его в OSGi из-за разницы в стиле загрузки классов.
Предложенное решение было, пожалуй, не самым простым. Другой подход основан на той же идее, что и в C++: закрытые члены недоступны за пределами пакета / частной области, за исключением определенного класса, который владелец делает себе другом.
Класс, которому требуется дружественный доступ к члену, должен создать внутренний общедоступный абстрактный «дружественный класс», к которому класс, владеющий скрытыми свойствами, может экспортировать доступ, возвращая подкласс, реализующий методы реализации доступа. Метод «API» класса друга может быть частным, поэтому он недоступен за пределами класса, которому требуется доступ друзей. Его единственное утверждение - это вызов абстрактного защищенного члена, который реализует экспортирующий класс.
Вот код:
Сначала тест, подтверждающий, что это действительно работает:
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());
}
}
Затем Сервис, которому нужен дружественный доступ к частному члену пакета 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();
}
}
Наконец: класс Entity, который обеспечивает удобный доступ к закрытому члену пакета только к классу 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();
}
};
}
}
Хорошо, я должен признать, что это немного длиннее, чем "Friend service :: Service;" но можно было бы сократить его, сохранив при этом проверку во время компиляции, используя аннотации.
Это не совсем работает, поскольку обычный класс в том же пакете может просто getFriend (), а затем вызвать защищенный метод, минуя частный.
В Java возможно «дружелюбие, связанное с пакетами». Это может быть полезно для модульного тестирования. Если вы не укажете private / public / protected перед методом, он будет «другом в пакете». Класс в том же пакете сможет получить к нему доступ, но он будет частным вне класса.
Это правило не всегда известно, и это хорошее приближение ключевого слова «друг» C++. Я считаю это хорошей заменой.
Это правда, но я действительно спрашивал о коде, находящемся в разных пакетах ...
Есть два решения вашего вопроса, которые не связаны с хранением всех классов в одном пакете.
Первый - использовать шаблон Friend Accessor / Пакет друзей, описанный в (Practical API Design, Tulach 2008).
Второй - использовать OSGi. Есть статья здесь, объясняющая, как OSGi это делает.
Эйрикма ответ прост и превосходен. Я мог бы добавить еще одну вещь: вместо общедоступного метода getFriend () для получения друга, который нельзя использовать, вы можете пойти еще дальше и запретить получение друга без токена: getFriend (Service.FriendToken). Этот FriendToken будет внутренним публичным классом с частным конструктором, так что только Service может создать его экземпляр.
Я думаю, что классы друзей в C++ похожи на концепцию внутреннего класса в Java. Использование внутренних классов на самом деле вы можете определить закрывающий класс и закрытый класс. Закрытый класс имеет полный доступ к открытым и закрытым членам закрывающего класса. см. следующую ссылку: http://docs.oracle.com/javase/tutorial/java/javaOO/nested.html
Э, нет, они не такие. Это больше похоже на дружбу в реальной жизни: она может, но не обязательно, быть взаимной (то, что А является другом Б, не означает, что Б считается другом А), и вы и ваши друзья можете быть совершенно разными людьми. семьи и иметь свой собственный, возможно (но не обязательно) пересекающийся круг друзей. (Не то чтобы я хотел видеть классы с большим количеством друзей. Это может быть полезной функцией, но ее следует использовать с осторожностью.)
Я согласен с тем, что в большинстве случаев ключевое слово друга не нужно.
И, наконец, если это действительно необходимо, в других ответах упоминается шаблон аксессуара друга.
Вот небольшой трюк, который я использую в JAVA для репликации механизма друга C++.
Допустим, у меня есть класс Romeo и другой класс Juliet. Они находятся в разных упаковках (семье) из соображений ненависти.
Romeo хочет, чтобы cuddleJuliet, а Juliet хочет только позволить Romeocuddle ей.
В C++ Juliet объявляет Romeo как (любитель) friend, но в java таких вещей нет.
Вот классы и трюк:
Дамы вперед :
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?");
}
}
Итак, метод Juliet.cuddle - это public, но для его вызова вам нужен Romeo.Love. Он использует этот Romeo.Love в качестве «защиты подписи», чтобы гарантировать, что только Romeo может вызывать этот метод, и проверяет, что любовь реальна, так что среда выполнения выдаст NullPointerException, если это null.
Теперь мальчики:
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);
}
}
Класс Romeo.Love является общедоступным, но его конструктор - private. Поэтому его может увидеть каждый, но построить его может только Romeo. Я использую статическую ссылку, поэтому Romeo.Love, который никогда не используется, создается только один раз и не влияет на оптимизацию.
Следовательно, Romeo может cuddleJuliet, и только он может, потому что только он может создать и получить доступ к экземпляру Romeo.Love, который требуется Juliet для cuddle ее (иначе она ударит вас NullPointerException).
Продолжение: я новичок в Java, поэтому я предполагаю, что нет атрибута (аннотации?), Который можно было бы использовать для обеспечения того, чтобы параметр "Romeo.Love" для "cuddle" не мог быть нулевым? Что-то похожее на '__attribute __ ((nonnull))' во многих компиляторах C? Это сделало бы это идеальным решением IMHO (поскольку оно все еще круто).
+1 за «пощечину вам исключение NullPointerException». Очень впечатляюще.
@Steazy Есть: ищите аннотации NotNull, NonNull и CheckForNull. Обратитесь к документации своей IDE, чтобы узнать, как использовать и применять эти аннотации. Я знаю, что IntelliJ встраивает это по умолчанию и что для eclipse нужен плагин (например, FindBugs).
@ChrisNash Вы не можете. Джульетта должна объявить аксессуар: public Whatever getPrivateField(Romeo.Love l) { l.hashCode(); return mPrivateField; }.
Вы можете сделать RomeoLove для Julia вечным, изменив поле love на final ;-).
... и если вы думаете, что Romeo был растафарианским «одним Love», вы можете сделать поле lovestatic ;-).
@Matthias Поле любви статично ... Я отредактирую ответ, чтобы сделать его окончательным;)
Еще один крутой трюк, чтобы сделать это еще более элегантным (хотя, возможно, и сбивающим с толку), - переименовать поле love в Love. Да, вы действительно можете это сделать (см. stackoverflow.com/a/14027255/1084488). В результате упоминания «Love» в коде Romeo (например, в Juliet.cuddle(Love);) будут интерпретироваться как ссылки на его everlasting, один объект Love (!), Тогда как упоминания Romeo.Love вне класса Romeo будут относиться к общедоступному классу Love. (!).
если бы мой предложение системы голосования не был так отвергнут, этот ответ получил бы «вдохновляющие 5 звезд» и «гениальные 5 звезд», поскольку вам удалось проявить хороший юмор и при этом предоставить отличную альтернативу «из коробки» :)
есть ли у вас совет для этот вопрос? это связано с идеей вашего ответа, спасибо!
Все ответы должны быть такими (Y) +1 за юмор и отличный пример.
Полностью согласен, не только этот ответ является отличным ответом, но и обеспечивает простой, но эффективный контекст для запоминания этого «шаблона». Прекрасная работа.
Совершенно новый смысл фразы «Мой код - поэзия».
Самый фантастический ответ, который я когда-либо читал в SO. +1 за вечную любовь Ромео по квалификатору final;)
Очень поздно на эту вечеринку, но я должен сказать: это самый великолепный отрывок из многослойной прозы, который я когда-либо читал. БРАВО. Я ГОВОРЮ БРАВО, ДОБРЫЙ Сэр!
Красивый! Хотя необходимость в подобном паттерне обычно является сильным намеком на запах дизайна, мы не всегда можем сразу провести большой рефакторинг, когда что-то пахнет подозрительно. В таких случаях это решение максимально элегантно.
Мне очень нравится ответ, но мне было интересно, можно ли заставить его сопротивляться созданию экземпляра путем отражения (например, как это сделал Gson). В противном случае любой может создать объект Romeo.Love.
@noamtm Против отражения мало что можно сделать. По задумке отражение позволяет делать практически все, включая вызов частных методов.
@Salomon BRYS, это не очень впечатляет, но я думаю, что это не работает. А как насчет класса Fake Romeo (назовем его Iago), который будет иметь член Iago.Love, который не создается сам по себе, а украден у Romeo (поскольку он общедоступен). Что-то вроде: приватный статический финал Love love = Rome.love
Lago не может построить Romeo.Love (частный конструктор) или получить доступ к Romeo.love (частная собственность).
Разве это нельзя было воспроизвести с помощью преобразования другого объекта в Romeo.Love?
Обратите внимание, что этот механизм / идиома (Идиома ключа доступа) также может быть реализован на C++ (поскольку он дает доступ только к некоторой части класса, а не ко всему классу).
Метод, который я нашел для решения этой проблемы, - это создать объект-аксессор, например:
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();
}
}
Первый код, вызывающий getAccessor(), «заявляет о праве собственности» на аксессор. Обычно это код, который создает объект.
Foo bar = new Foo(); //This object is safe to share.
FooAccessor barAccessor = bar.getAccessor(); //This one is not.
Это также имеет преимущество перед механизмом друга C++, потому что он позволяет вам ограничивать доступ на уровне на экземпляр, в отличие от уровня за класс. Управляя ссылкой на метод доступа, вы управляете доступом к объекту. Вы также можете создать несколько средств доступа и предоставить разный доступ к каждому, что позволяет детально контролировать, какой код может получить доступ к чему:
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();
}
}
Наконец, если вы хотите, чтобы все было немного более организовано, вы можете создать ссылочный объект, который объединяет все вместе. Это позволяет вам запрашивать все аксессоры одним вызовом метода, а также хранить их вместе с их связанным экземпляром. Получив ссылку, вы можете передать аксессоры коду, который в ней нуждается:
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();
}
}
После долгих головокружений (не самого лучшего) это было моим окончательным решением, и оно мне очень понравилось. Он гибкий, простой в использовании и позволяет очень хорошо контролировать доступ к классам. (Доступ только со ссылкой очень полезен.) Если вы используете protected вместо private для средств доступа / ссылок, подклассы Foo могут даже возвращать расширенные ссылки из getReference. Он также не требует отражения, поэтому его можно использовать в любой среде.
Вот наглядный пример использования многоразового класса Friend. Преимущество этого механизма - простота использования. Может быть, хорошо для предоставления классов модульного тестирования большего доступа, чем остальной части приложения.
Для начала приведем пример использования класса Friend.
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;
}
}
Тогда в другом пакете вы можете сделать это:
public class Other {
private final Friend friend = new Friend(this);
public void test() {
String s = new Owner().getMember(friend);
System.out.println(s);
}
}
Класс Friend выглядит следующим образом.
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);
}
}
Однако проблема в том, что им можно злоупотреблять так:
public class Abuser {
public void doBadThings() {
Friend badFriend = new Friend(new Other());
String s = new Owner().getMember(badFriend);
System.out.println(s);
}
}
Теперь может быть правдой, что класс Other не имеет никаких общедоступных конструкторов, что делает невозможным приведенный выше код Abuser. Однако, если ваш класс делает имеет общедоступный конструктор, вероятно, целесообразно продублировать класс Friend как внутренний класс. В качестве примера возьмем этот класс Other2:
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);
}
}
И тогда класс Owner2 будет таким:
public class Owner2 {
private final String member = "value";
public String getMember(final Other2.Friend friend) {
friend.check();
return member;
}
}
Обратите внимание, что у класса Other2.Friend есть частный конструктор, что делает этот способ гораздо более безопасным.
Я думаю, что подход к использованию шаблона аксессуара друга слишком сложен. Мне пришлось столкнуться с той же проблемой, и я решил использовать старый добрый конструктор копирования, известный по C++, на 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;
}
}
В своем приложении вы можете написать следующий код:
public class MyApp {
private static class ProtectedAccessor extends ProtectedContainer {
protected ProtectedAccessor() {
super();
}
protected PrivateAccessor(ProtectedContainer prot) {
super(prot);
}
public String exposeProtected() {
return iwantAccess;
}
}
}
Преимущество этого метода в том, что только ваше приложение имеет доступ к защищенным данным. Это не совсем подмена ключевого слова друга. Но я думаю, что это вполне подходит, когда вы пишете собственные библиотеки и вам нужен доступ к защищенным данным.
Всякий раз, когда вам приходится иметь дело с экземплярами ProtectedContainer, вы можете обернуть его своим ProtectedAccessor, и вы получите доступ.
Он также работает с защищенными методами. Вы определяете их как защищенные в своем API. Позже в своем приложении вы напишете частный класс-оболочку и сделаете защищенный метод общедоступным. Вот и все.
Но ProtectedContainer можно разделить на подклассы вне пакета!
Начиная с Java 9, во многих случаях для решения этой проблемы можно использовать модули.
Я бы посчитал это настолько плохой идеей, что даже предполагать это мне противно. Очевидно, что это в лучшем случае лабиринт, и он не должен быть частью какого-либо дизайна.