Подскажите, пожалуйста, планируете ли вы возобновление программ высшего образования? Если да, есть ли какие-то примерные сроки? Спасибо! |
Основные конструкции языков Java и C# (продолжение)
Элементы типов
Элементы или члены (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.
Для многих из дополнительных разновидностей членов типов, имеющихся в 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. |
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# различается достаточно сильно.