Компонентный подход в программировании


Элементы типов


Элементы или члены (members) пользовательских типов могут быть методами, полями (в классах) и вложенными типами. В классе можно также объявлять конструкторы, служащие для создания объектов этого класса. В обоих языках описание конструктора похоже на описание метода, только тип результата не указывается, а вместо имени метода используется имя самого класса.

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

У каждого элемента класса могут присутствовать модификаторы, определяющие доступность этого элемента из разных мест программы, а также его контекст — относится ли он к объектам этого класса (нестатический элемент) или к самому классу (статический элемент, помечается как static).

Для указания доступности в обоих языках могут использоваться модификаторы public, protected и private, указывающие, соответственно, что данный элемент доступен везде, где доступен содержащий его тип, доступен только в описаниях типов-наследников содержащего типа или только в рамках описания самого содержащего типа. Доступность по умолчанию, без указания модификатора, трактуется в рассматриваемых языках различно.

Нестатические методы в обоих языках (а также свойства, индексированные свойства и события в C#) могут быть объявлены абстрактными (abstract), т.е. не задающими реализации соответствующей операции. Такие методы (а также свойства, индексированные свойства и события в C#) помечаются модификатором abstract. Вместо кода у абстрактного метода сразу после описания полной сигнатуры идет точка с запятой.

Методы (свойства, индексированные свойства и события в C#), которые не должны быть перегружены в наследниках содержащего их класса, помечаются в Java как final, а в C#, как sealed.

В обоих языках можно использовать операции, реализованные на других языках. Для этого в C# используются стандартные механизмы .NET — класс реализуется на одном из языков, поддерживаемых .NET, с учетом ограничений на общие библиотеки этой среды, и становится доступен из любого другого кода на поддерживаемом .NET языке.


В Java для этого предусмотрен механизм Java Native Interface, JNI [6,7]. Класс Java может иметь ряд внешних методов, помеченных модификатором native. Вместо кода у таких методов сразу после описания полной сигнатуры идет точка с запятой. Они по определенным правилам реализуются в виде функций на языке C (или на другом языке, если можно в результате компиляции получить библиотеку с интерфейсом на C). Внешние методы, а также свойства, индексированные свойства, события и операторы, привязываемые по определенным правилам к функциям, которые имеют интерфейс на C и не вложены в среду .NET, есть и в C# — там такие операции помечаются как extern.



В Java, помимо перечисленных членов типов, имеются инициализаторы. Их описание приведено ниже.

Инициализаторы относятся только к тому классу, в котором они определены, их нельзя перегрузить.


В C# члены типов, помимо методов, полей, конструкторов и вложенных типов, могут быть константами, свойствами, индексированными свойствами, событиями или операторами. Кроме этого, в типе можно определить деструктор и статический конструктор.

Нестатические свойства, индексированные свойства и события можно перегружать в наследниках. Остальные из перечисленных элементов относятся только к тому классу, в котором описаны.
Для многих из дополнительных разновидностей членов типов, имеющихся в C#, есть аналогичные идиомы в компонентной модели JavaBeans [8,9], предназначенной для построения элементов пользовательского интерфейса и широко используемой в рамках Java-технологий для создания компонентов, структуру которых можно анализировать динамически на основе предлагаемых JavaBeans соглашений об именовании методов. Далее вместе с примерами кода на C# в правом столбце в левом приводится аналогичный код, написанный в соответствии с JavaBeans.



Константы в Java принято оформлять в виде полей с модификаторами final static. Модификатор final для поля означает, что присвоить ему значение можно только один раз и сделать это нужно либо в статическом инициализаторе класса (см. ниже), если поле статическое, либо в каждом из конструкторов, если поле нестатическое:


Константы являются единожды вычисляемыми и неизменными далее значениями, хранящимися в классе или структуре.

Пример объявления константы приведен ниже.


public class A2 { public static final double PHI = 1.61803398874989; }
public class A2 { public const double Phi = 1.61803398874989; }


Компонентная модель JavaBeans определяет свойство (property) класса A, имеющее имя name и тип T, как набор из одного или двух методов, декларированных в классе A — T getName() и void setName(T), называемых методами доступа (accessor methods) к свойству.



Свойство может быть доступным только для чтения, если имеется лишь метод get, и только для записи, если имеется лишь метод set.

Если свойство имеет логический тип, для метода чтения этого свойства используется имя isName().

Эти соглашения широко используются в разработке Java-программ, и такие свойства описываются не только у классов, предназначенных стать компонентами JavaBeans.

Они и стали основанием для введения специальной конструкции для описания свойств в C#.


Свойства (properties) представляют собой "виртуальные" поля. Каждое свойство имеет один или оба метода доступа get и set, которые определяют действия, выполняемые при чтении и модификации этого свойства. Оба метода доступа описываются внутри декларации свойства. Метод set использует специальный идентификатор value для ссылки на устанавливаемое значение свойства.

Обращение к свойству — чтение (возможно, только если у него есть метод get) или изменение значения свойства (возможно, только если у него есть метод set) — происходит так же, как к полю.

При перегрузке свойства в наследниках перегружаются методы доступа к нему.

Пример объявления свойств и их использования приведен ниже.


public class MyArrayList { private int[] items = new int[10]; private int size = 0;

public int getSize() { return size; }

public int getCapacity() { return items.Length; }

public void setCapacity(int value) { int[] newItems = new int[value]; System.arraycopy (items, 0, newItems, 0, size); items = newItems; }

public static void main(String[] args) { MyArrayList l = new MyArrayList(); System.out.println(l.getSize()); System.out.println(l.getCapacity()); l.setCapacity(50); System.out.println(l.getSize()); System.out.println(l.getCapacity()); } }
using System;

public class MyArrayList { private int[] items = new int[10]; private int size = 0;

public int Size { get { return size; } }

public int Capacity { get { return items.Length; } set { int[] newItems = new int[value]; Array.Copy (items, newItems, size); items = newItems; } }

public static void Main() { MyArrayList l = new MyArrayList(); Console.WriteLine( l.Size ); Console.WriteLine( l.Capacity ); l.Capacity = 50; Console.WriteLine( l.Size ); Console.WriteLine( l.Capacity ); } }


JavaBeans определяет индексированное свойство (indexed property) класса A, имеющее имя name и тип T, как один или пару методов T getName(int) и void setName(int, T).

Свойства могут быть индексированы только одним целым числом. В дальнейшем предполагалось ослабить это ограничение и разрешить индексацию несколькими параметрами, которые могли бы иметь разные типы. Однако с 1997 года, когда появилась последняя версия спецификаций JavaBeans [9], этого пока сделано не было.


Индексированное свойство или индексер (indexer) — это свойство, зависящее от набора параметров.

В C# может быть определен только один индексер для типа и данного набора типов параметров. Т.е. нет возможности определять свойства с разными именами, но одинаковыми наборами индексов.

Обращение к индексеру объекта (или класса, т.е. статическому) производится так, как будто этот объект (класс) был бы массивом, индексированным набором индексов соответствующих типов.

При перегрузке индексера в наследниках перегружаются методы доступа к нему.

Индексеры должны быть нестатическими.

Обращение к индексеру класса-предка в индексере наследника организуется с помощью конструкции base[…].

Пример декларации и использования индексера приведен ниже.



public class MyArrayList { int[] items = new int[10]; int size = 0;

public int getItem(int i) { if (i < 0 || i >= 10) throw new IllegalArgumentException(); else return items[i]; }

public void setItem(int i, int value) { if (i < 0 || i >= 10) throw new IllegalArgumentException(); else items[i] = value; }

public static void main(String[] args) { MyArrayList l = new MyArrayList(); l.setItem(0, 23); l.setItem(1, 75); l.setItem(1, l.getItem(1)-1); l.setItem(0, l.getItem(0) + l.getItem(1)); System.out.println (l.getItem(0)); System.out.println (l.getItem(1)); } }
using System;

public class MyArrayList { int[] items = new int[10]; int size = 0;

public int this[int i] { get { if (i < 0 || i >= 10) throw new IndexOutOfRangeException(); else return items[i]; } set { if (i < 0 || i >= 10) throw new IndexOutOfRangeException(); else items[i] = value; } }

public static void Main() { MyArrayList l = new MyArrayList(); l[0] = 23; l[1] = 75; l[0] += (--l[1]); Console.WriteLine(l[0]); Console.WriteLine(l[1]); } }


События (events) в модели JavaBeans служат для оповещения набора объектов-наблюдателей (listeners) о некоторых изменениях в состоянии объекта-источника (source).

При этом класс EventType объектов, представляющих события определенного вида, должен наследовать java.util.EventObject. Все объекты-наблюдатели должны реализовывать один интерфейс EventListener, в котором должен быть метод обработки события (обычно называемый так же, как и событие) с параметром типа EventType. Интерфейс EventListener должен наследовать интерфейсу java.util.EventListener.

Класс источника событий должен иметь методы для регистрации наблюдателей и их удаления из реестра. Эти методы должны иметь сигнатуры

public void addEventListener (EventListener) public void removeEventListener (EventListener).

Можно заметить, что такой способ реализации обработки событий воплощает образец проектирования "Подписчик".

В приведенном ниже примере все public классы и интерфейсы должны быть описаны в разных файлах.


Событие (event) представляет собой свойство специального вида, имеющее делегатный тип. У события, в отличие от обычного свойства, методы доступа называются add и remove и предназначены они для добавления или удаления обработчиков данного события, являющихся делегатами (это аналоги различных реализаций метода обработки события в интерфейсе наблюдателя в JavaBeans) при помощи операторов += и =.

Событие может быть реализовано как поле делегатного типа, помеченное модификатором event. В этом случае декларировать соответствующие методы add и remove необязательно — они автоматически реализуются при применении операторов += и = в к этому полю как к делегату.

Если же программист хочет реализовать какое-то специфическое хранение обработчиков события, он должен определить методы add и remove.

В приведенном ниже примере одно из событий реализовано как событие-поле, а другое — при помощи настоящего поля и методов add и remove, дающих совместно тот же результат.

При перегрузке события в наследниках перегружаются методы доступа к нему.


public class MouseEventArgs { ... }

public class MouseEventObject extends java.util.EventObject { MouseEventArgs args;

MouseEventObject (Object source, MouseEventArgs args) { super(source); this.args = args; } }

public interface MouseEventListener extends java.util.EventListener { void mouseUp(MouseEventObject e); void mouseDown(MouseEventObject e); }

import java.util.ArrayList;

public class MouseEventSource { private ArrayList<MouseEventListener> listeners = new ArrayList <MouseEventListener >();

public synchronized void addMouseEventListener (MouseEventListener l) { listeners.add(l); }

public synchronized void removeMouseEventListener (MouseEventListener l) { listeners.remove(l); }

protected void notifyMouseUp (MouseEventArgs a) { MouseEventObject e = new MouseEventObject(this, a); ArrayList<MouseEventListener> l; synchronized(this) { l = (ArrayList<MouseEventListener>) listeners.clone(); for(MouseEventListener el : l) el.mouseUp(e); } }

protected void notifyMouseDown (MouseEventArgs a) { MouseEventObject e = new MouseEventObject(this, a);

ArrayList<MouseEventListener> l; synchronized(this) { l = (ArrayList<MouseEventListener>) listeners.clone(); for(MouseEventListener el : l) el.mouseDown(e); } } }

public class HandlerConfigurator { MouseEventSource s = new MouseEventSource();

MouseEventListener listener = new MouseEventListener() { public void mouseUp (MouseEventObject e) { ... } public void mouseDown (MouseEventObject e) { ... } };

public void configure() { s.addMouseEventListener(listener); } }
public class MouseEventArgs { ... }

public delegate void MouseEventHandler (object source, MouseEventArgs e);

public class MouseEventSource { public event MouseEventHandler MouseUp;

private MouseEventHandler mouseDown; public event MouseEventHandler MouseDown { add { lock(this) { mouseDown += value; } } remove { lock(this) { mouseDown -= value; } } }

protected void OnMouseUp(MouseEventArgs e) { MouseUp(this, e); } protected void OnMouseDown(MouseEventArgs e) { mouseDown(this, e); } }

public class HandlerConfigurator { MouseEventSource s = new MouseEventSource();

public void UpHandler (object source, MouseEventArgs e) { ... } public void DownHandler (object source, MouseEventArgs e) { ... }

public void Configure() { s.MouseUp += UpHandler; s.MouseDown += DownHandler; } }


Методы доступа к свойствам, индексерам или событиям в классах-наследниках могут перегружаться по отдельности, т.е., например, метод чтения свойства перегружается, а метод записи — нет.

В C# 2.0 введена возможность декларации различной доступности у таких методов. Например, метод чтения свойства можно сделать общедоступным, а метод записи — доступным только для наследников.

Для этого можно описать свойство так:

public int Property { get { … } protected set { … } }


В Java никакие операторы переопределить нельзя.

Вообще, в этом языке имеются только операторы, действующие на значениях примитивных типах, сравнение объектов на равенство и неравенство, а также оператор + для строк (это объекты класса java.lang.String), обозначающий операцию конкатенации.

Оператор + может применяться и к другим типам аргументов, если один из них имеет тип String. При этом результатом соответствующей операции является конкатенация его и результата применения метода toString() к другому операнду в порядке следования операндов.


Некоторые операторы в C# можно переопределить (перекрыть) для данного пользовательского типа. Переопределяемый оператор всегда имеет модификатор static.

Переопределяемые унарные операторы (их единственный параметр должен иметь тот тип, в рамках которого они переопределяются, или объемлющий тип): +, -, !, ~ (в качестве типа результата могут иметь любой тип), ++,-- (тип их результата может быть только подтипом объемлющего), true, false (тип результата bool).

Переопределяемые бинарные операторы (хотя бы один из их параметров должен иметь объемлющий тип, а возвращать они могут результат любого типа): +, -, *, /, %, &, |, ^, <<, >>, ==, !=, <, >, <=, >=. Для операторов сдвига << и >> ограничения более жесткие — первый их параметр должен иметь объемлющий тип, а второй быть типа int.

Можно определять также операторы приведения к другому типу или приведения из другого типа, причем можно объявить такое приведение неявным с помощью модификатора implicit, чтобы компилятор сам вставлял его там, где оно необходимо для соблюдения правил соответствия типов. Иначе надо использовать модификатор explicit и всегда явно приводить один тип к другому.

Некоторые операторы можно определять только парами — таковы true и false, == и !=, < и >, <= и >=.

Операторы true и false служат для неявного преобразования объектов данного типа к соответствующим логическим значениям.

Если в типе T определяются операторы & и |, возвращающие значение этого же типа, а также операторы true и false, то к объектам типа можно применять условные логические операторы && и ||. Их поведение в этом случае может быть описано соотношениями (x && y) = (T.false(x)? x : (x & y)) и (x || y) = (T.true(x)? x : (x | y)).

Аналогичным образом автоматически переопределяются составные операторы присваивания, если переопределены операторы +, -, *, /, %, &, |, ^, << или >>.

Ниже приведен пример переопределения и использования операторов. Обратите внимание, что оператор приведения типа MyInt в int действует неявно, а для обратного перехода требуется явное приведение.

Другая тонкость — необходимость приведения объектов типа MyInt к object в методе AreEqual — если этого не сделать, то при обращении к оператору == возникнет бесконечный цикл, поскольку сравнение i1 == null тоже будет интерпретироваться как вызов этого оператора.

using System;

public class MyInt { int n = 0;

public MyInt(int n) { this.n = n; }

public override bool Equals(object obj) { MyInt o = obj as MyInt; if (o == null) return false; return o.n == n; }

public override int GetHashCode() { return n; }

public override string ToString() { return n.ToString(); }

public static bool AreEqual (MyInt i1, MyInt i2) { if ((object)i1 == null) return ((object)i2 == null); else return i1.Equals(i2); }

public static bool operator == (MyInt i1, MyInt i2) { return AreEqual(i1, i2); }

public static bool operator != (MyInt i1, MyInt i2) { return !AreEqual(i1, i2); }

public static bool operator true (MyInt i) { return i.n > 0; }

public static bool operator false (MyInt i) { return i.n <= 0; }

public static MyInt operator ++ (MyInt i) { return new MyInt(i.n + 1); }

public static MyInt operator -- (MyInt i) { return new MyInt(i.n - 1); }

public static MyInt operator & (MyInt i1, MyInt i2) { return new MyInt(i1.n & i2.n); }

public static MyInt operator | (MyInt i1, MyInt i2) { return new MyInt(i1.n | i2.n); }

public static implicit operator int (MyInt i) { return i.n; }

public static explicit operator MyInt (int i) { return new MyInt(i); }

public static void Main() { MyInt n = (MyInt)5; MyInt k = (MyInt)(n - 3 * n); Console.WriteLine("k = " + k + " , n = " + n); Console.WriteLine("n == n : " + (n == n)); Console.WriteLine("n == k : " + (n == k)); Console.WriteLine( "(++k) && (n++) : " + ((++k) && (n++))); Console.WriteLine("k = " + k + " , n = " + n); Console.WriteLine( "(++n) && (k++) : " + ((++n) && (k++))); Console.WriteLine("k = " + k + " , n = " + n); } }


Аналогом деструктора в Java является метод protected void finalize(), который можно перегрузить для данного класса.

Так же, как и деструктор в C#, этот метод вызывается на некотором шаге уничтожения объекта после того, как тот был помечен сборщиком мусора как неиспользуемый.


Деструктор предназначен для освобождения каких-либо ресурсов, связанных с объектом и не освобождаемых автоматически средой .NET, либо для оптимизации использования ресурсов за счет их явного освобождения.

Деструктор вызывается автоматически при уничтожении объекта в ходе работы механизма управления памятью .NET. В этот момент объект уже должен быть помечен сборщиком мусора как неиспользуемый.

Деструктор оформляется как особый метод, без возвращаемого значения и с именем, получающимся добавлением префикса ‘~’ к имени класса


public class MyFileReader { java.io.FileReader input;

public MyFileReader(String path) throws FileNotFoundException { input = new java.io.FileReader (new java.io.File(path)); }

protected void finalize() { System.out.println("Destructor"); try { input.close(); } catch (IOException e) { e.printStackTrace(); } } }
using System;

public class MyFileStream { System.IO.FileStream input;

public MyFileStream(string path) { input = System.IO.File.Open (path, System.IO.FileMode.Open); }

~MyFileStream() { Console.WriteLine("Destructor"); input.Close(); } }


Инициализаторы представляют собой блоки кода, заключенные в фигурные скобки и расположенные непосредственно внутри декларации класса.

Эти блоки выполняются вместе с инициализаторами отдельных полей — выражениями, которые написаны после знака = в объявлениях полей — при построении объекта данного класса, в порядке их расположения в декларации.


Статическиеинициализаторы — такие же блоки, помеченные модификатором static — выполняются вместе с инициализаторами статических полей по тем же правилам в момент первой загрузки класса в Java-машину.


Статический конструктор класса представляет собой блок кода, выполняемый при первой загрузке класса в среду .NET, т.е. в момент первого использования этого класса в программе. Это аналог статического инициализатора в Java.

Оформляется он как конструктор с модификатором static.


public class A { static { System.out.println("Loading A"); }

static int x = 1;

static { System.out.println("x = " + x); x++; }

static int y = 2;

static { y = x + 3; System.out.println("x = " + x); System.out.println("y = " + y); }

public static void main(String[] args) {} }
using System;

public class A { static A() { Console.WriteLine("Loading A"); Console.WriteLine("x = " + x); x++; y = x + 3; Console.WriteLine("x = " + x); Console.WriteLine("y = " + y); }

static int x = 1;

static int y = 2;

public static void Main() {} }


Приведенный выше код выдает результат

Loading A x = 1 x = 2 y = 5


Приведенный выше код выдает результат

Loading A x = 1 x = 2 y = 5


В Java нестатические вложенные типы трактуются очень специфическим образом — каждый объект такого типа считается привязанным к определенному объекту объемлющего типа. У нестатического вложенного типа есть как бы необъявленное поле, хранящее ссылку на объект объемлющего типа.

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

Получить этот объект внутри декларации вложенного типа можно с помощью конструкции ClassName.this, где ClassName — имя объемлющего типа.

При создании объекта такого вложенного класса необходимо указать объект объемлющего класса, к которому тот будет привязан.


В C# модификатор static у класса, все равно, вложенного в другой или нет, обозначает, что этот класс является контейнером набора констант и статических операций. Все его элементы должны быть декларированы как static.


public class ContainingClass { static int counter = 1; static int ecounter = 1; int id = counter++;

class EmbeddedClass { int eid = ecounter++;

public String toString() { return "" + ContainingClass.this.id + '.' + eid; } }

public String toString() { return "" + id; }

public static void main (String[] args) { ContainingClass c = new ContainingClass() , c1 = new ContainingClass(); System.out.println(c); System.out.println(c1);

EmbeddedClass e = c.new EmbeddedClass() , e1 = c.new EmbeddedClass() , e2 = c1.new EmbeddedClass(); System.out.println(e); System.out.println(e1); System.out.println(e2); } }


В C# класс может определить различные реализации для операций (методов, свойств, индексеров, событий) с одинаковой сигнатурой, если они декларированы в различных реализуемых классом интерфейсах.

Для этого при определении таких операций нужно указывать имя интерфейса в качестве расширения их имени.

using System;

public interface I1 { void m(); }

public interface I2 { void m(); }

public class A : I1, I2 { public void m() { Console.WriteLine("A.m() called"); }

void I1.m() { Console.WriteLine ("I1.m() defined in A called"); }

void I2.m() { Console.WriteLine ("I2.m() defined in A called"); }

public static void Main() { A f = new A(); I1 i1 = f; I2 i2 = f;

f.m(); i1.m(); i2.m(); } }

Результат работы приведенного выше примера следующий.

A.m() called I1.m() defined in A called I2.m() defined in A called
<


Последовательность выполнения инициализаторов полей и конструкторов классов-предков и наследников при построении объектов в Java и C# различается достаточно сильно.

О правилах, определяющих эту последовательность, можно судить по результатам работы следующих примеров.



public class A { static { System.out.println ("Static initializer of A"); }

{ System.out.println ("Initializer of A"); }

static int sinit() { System.out.println ("Static field initializer of A"); return 0; }

static int init() { System.out.println ("Field initializer of A"); return 0; }

static int sf = sinit(); int f = init();

public A() { System.out.println ("Constructor of A"); } }

public class B extends A { static int sf = sinit(); int f = init();

static { System.out.println ("Static initializer of B"); }

{ System.out.println ("Initializer of B"); }

static int sinit() { System.out.println ("Static field initializer of B"); return 0; }

static int init() { System.out.println ("Field initializer of B"); return 0; }

public B() { System.out.println ("Constructor of B"); } }

public class C { public static void main(String[] args) { B b = new B(); } }
using System;

public class A { static A() { Console.WriteLine ("Static constructor of A"); }

static int sinit() { Console.WriteLine ("Static field initializer of A"); return 0; }

static int init() { Console.WriteLine ("Field initializer of A"); return 0; }

static int sf = sinit(); int f = init();

public A() { Console.WriteLine ("Constructor of A"); } }

public class B : A { static int sf = sinit(); int f = init();

static B() { Console.WriteLine ("Static constructor of B"); }

static int sinit() { Console.WriteLine ("Static field initializer of B"); return 0; }

static int init() { Console.WriteLine ("Field initializer of B"); return 0; }

public B() { Console.WriteLine ("Constructor of B"); } }

public class C { public static void Main() { B b = new B(); } }


Результат работы приведенного примера такой.

Static initializer of A Static field initializer of A Static field initializer of B Static initializer of B Initializer of A Field initializer of A Constructor of A Field initializer of B Initializer of B Constructor of B


Результат работы приведенного примера такой.

Static field initializer of B Static constructor of B Field initializer of B Static field initializer of A Static constructor of A Field initializer of A Constructor of A Constructor of B


В Java элемент типа, помимо доступности, указываемой с помощью модификаторов private, protected и public, может иметь пакетную доступность. Такой элемент может использоваться в типах того же пакета, к которому относится содержащий его тип.

Именно пакетная доступность используется в Java по умолчанию.

protected элементы в Java также доступны из типов того пакета, в котором находится содержащий эти элементы тип, т.е. protected-доступность шире пакетной.

Типы, не вложенные в другие, могут быть либо public (должно быть не более одного такого типа в файле), либо иметь пакетную доступность.


В C# доступность элемента типа по умолчанию — private.

Имеется дополнительный модификатор доступности — internal. Элемент, имеющий такую доступность, может быть использован всюду в рамках того же файла, где находится определение этого элемента.

Кроме того, в C# можно использовать комбинированный модификатор protected internal для указания того, что к данному элементу имеют доступ как наследники содержащего его типа, так и типы в рамках содержащего его файла.

Типы, не вложенные в другие, могут быть либо public, либо иметь доступность по умолчанию — private, что означает, что они могут использоваться только в рамках содержащего их пространства имен.

В C# все элементы типов могут быть помечены модификатором new для точного указания на то, что такой элемент — новый и никак не соотносится с элементами предков данного типа с тем же именем или той же сигнатурой.


В Java для полей классов дополнительно к модификаторам доступности и контекста могут использоваться модификаторы final, transient и volatile.


Поля классов или структурных типов в C# могут иметь в качестве дополнительных модификаторов readonly и volatile.


Модификатор final обозначает, что такое поле не может быть изменено во время работы, но сначала должно быть инициализировано: статическое — в одном из статических инициализаторов, нестатическое — к концу работы каждого из конструкторов. В инициализаторах или конструкторах такое поле может модифицироваться несколько раз.

Модификатор final у локальных переменных и параметров методов может использоваться примерно в том же значении — невозможность модификации их значений после инициализации.


Модификатор readonly по своему значению аналогичен модификатору final в Java. readonly поля должны инициализироваться к концу работы конструкторов и дальше не могут быть изменены.

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


Поля, помеченные модификатором transient, считаются не входящими в состояние объекта или класса, подлежащее хранению или передаче по сети.

В Java имеются соглашения о том, как должен быть оформлен интерфейс класса, объекты которого могут быть сохранены или переданы по сети — эти соглашения можно найти в документации по интерфейсу java.io.Serializable, который должен реализовываться таким классом. Имеются и другие подобные наборы соглашений, привязанные к определенным библиотекам или технологиям в рамках Java.

Стандартный механизм Java, обеспечивающий сохранение и восстановление объектов, реализующих интерфейс java.io.Serializable, по умолчанию сохраняет значения всех полей, кроме помеченных модификатором transient.

Методы классов в Java могут быть дополнительно помечены модификаторами strictfp (такой же модификатор могут иметь инициализаторы) и synchronized.

Значение модификатора strictfp описано в Лекции 10, в разделе о типах с плавающей точкой.

Значение модификатора synchronized описывается ниже, в разделе, посвященном многопоточным приложениям.

Содержание раздела