Имеют ли смысл эффективно неизменяемые объекты? - PullRequest
10 голосов
/ 03 января 2012

В книге Параллелизм Java на практике он объясняет преимущества «эффективно неизменяемых» объектов по сравнению с изменчивыми объектами с точки зрения параллелизма. Но это не объясняет, какое преимущество "эффективно неизменяемые" объекты будут иметь над действительно неизменяемыми объектами.

И я не понимаю: разве вы не всегда можете создать действительно неизменяемый объект в тот момент, когда вы решите безопасно опубликовать "эффективно неизменный" объект? (вместо того, чтобы делать «безопасную публикацию», вы построите действительно неизменный объект и все)

Когда я проектирую классы, мне не удается увидеть случаи, когда я не всегда мог создать действительно неизменный объект (используя делегирование при необходимости и т. Д. Для создания других обернутых объектов, которые, конечно, действительно неизменны), в настоящий момент я бы решите «безопасно опубликовать».

Значит, «эффективно неизменяемый» объект и его «безопасная публикация» - это просто случай плохого дизайна или плохих API?

Где бы вы были вынуждены использовать эффективно неизменяемый объект и были бы вынуждены безопасно опубликовать его там, где вы не смогли бы построить гораздо более совершенный действительно неизменный объект?

Ответы [ 4 ]

9 голосов
/ 03 января 2012

Да, они имеют смысл в некоторых случаях.Простой пример - когда вы хотите, чтобы какое-то свойство генерировалось лениво и кэшировалось, чтобы вы могли избежать накладных расходов на его генерацию, если к нему никогда не обращаются.String является примером эффективно неизменяемого класса, который делает это (со своим хеш-кодом).

3 голосов
/ 03 января 2012

Для круговых неизменяемых:

class Foo
{
    final Object param;
    final Foo other;

    Foo(Object param, Foo other)
    {
        this.param = param;
        this.other = other;
    }

    // create a pair of Foo's, A=this, B=other
    Foo(Object paramA, Object paramB)
    {
        this.param = paramA;
        this.other = new Foo(paramB, this);
    }

    Foo getOther(){ return other; }
}



// usage
Foo fooA = new Foo(paramA, paramB);
Foo fooB = fooA.getOther();
// publish fooA/fooB (unsafely)

Вопрос в том, что this из fooA просочилась внутрь конструктора, является ли fooA по-прежнему безопасным для потоков? То есть, если другой поток читает fooB.getOther().param, гарантированно ли он увидит paramA? Ответ - да, поскольку this не просочился в другой поток до действия freeze ; мы можем установить порядки hb / dc / mc, требуемые спецификацией, чтобы доказать, что paramA является единственным видимым значением для чтения.

Вернуться к исходному вопросу. На практике всегда есть ограничения, помимо чисто технических. Инициализация всего внутри конструктора не обязательно является лучшим вариантом для проектирования, учитывая все инженерные, эксплуатационные, политические и другие человеческие причины.

Вы когда-нибудь задумывались над тем, почему нам надоело думать, что это великая высшая идея?

Более глубокой проблемой является Java. не хватает общего дешевого средства для безопасной публикации, которое дешевле, чем изменчивое. У Java это есть только для final полей; по какой-то причине этот забор недоступен.

Теперь final имеет два независимых значения: 1-е, что последнее поле должно быть назначено ровно один раз; Во-вторых, семантика памяти безопасной публикации. Эти два значения не имеют ничего общего друг с другом. Это довольно запутанно, чтобы связать их вместе. Когда люди нуждаются во втором значении, они также вынуждены принимать первое значение. Когда 1-е очень неудобно для достижения в дизайне, люди задаются вопросом, что они сделали неправильно - не понимая, что это неправильно сделала Java.

Объединение двух значений под одним final делает его двойным плюс хорошим, так что, очевидно, у нас больше оснований и мотивации для использования final. Более зловещая история - на самом деле мы вынуждены использовать ее, потому что нам не дают более гибкого выбора.

1 голос
/ 03 января 2012

Использование эффективно неизменяемых объектов позволяет избежать создания значительного числа классов. Вместо создания пар классов [изменяемый строитель] / [неизменяемый объект], вы можете создать один эффективно неизменный класс. Я обычно определяю неизменный интерфейс и изменяемый класс, который реализует этот интерфейс. Объект настраивается с помощью методов изменяемого класса, а затем публикуется через его неизменный интерфейс. Пока клиенты вашей библиотечной программы подключаются к интерфейсу, ваши объекты остаются неизменными в течение всего опубликованного времени жизни.

0 голосов
/ 02 июля 2012

Предположим, что у каждого есть неизменный класс Foo с пятью свойствами с именами Alpha, Beta и т. Д., И один хочет предоставить методы WithAlpha, WithBeta и т. Д., Которые будут возвращать экземпляр, который идентичен оригиналу, за исключением того, что конкретное свойство изменено. Если класс истинно и глубоко неизменен, методы должны принимать форму:

Foo WithAlpha(string newAlpha)
{ 
  return new Foo(newAlpha, Beta, Gamma, Delta, Epsilon);
}

Foo WithBeta(string newBeta) 
{
  return new Foo(Alpha, NewBeta, Gamma, Delta, Epsilon);
}

Ик. Массовое нарушение принципов "Не повторяйся" (СУХОЙ). Кроме того, добавление нового свойства в класс потребует добавления его в каждый из этих методов.

С другой стороны, если каждый Foo содержит внутренний FooGuts, который включает конструктор копирования, можно вместо этого сделать что-то вроде:

Foo WithAlpha(string newAlpha)
{
  FooGuts newGuts = new FooGuts(Guts); // Guts is a private or protected field
  newGuts.Alpha = newAlpha;
  return new Foo(newGuts); // Private or protected constructor
}

Количество строк кода для каждого метода увеличилось, но методам больше не нужно ссылаться на какие-либо свойства, в которых они не «заинтересованы». Обратите внимание, что хотя Foo может быть неизменным, если его конструктор вызывался с FooGuts, на который существовала любая внешняя ссылка, его конструктор доступен только к коду, которому доверяют не сохранять такую ​​ссылку после построения.

...