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