Сообщений 68    Оценка 860 [+0/-2]         Оценить  
Система Orphus

Adding notnull to the Java Programming Language

Автор: Dmytro Sheyko
Опубликовано: 08.08.2004
Исправлено: 10.12.2016
Версия текста: 1.3

Основная идея
Введение
Ненулевые ссылочные значения
Модификатор notnull
Оператор приведения нулабилити
Предопределенные ненулевые ссылочные значения
Производные ненулевые ссылочные значения
Инициализация переменных
Переопределение методов при наследовании
Перегрузка методов
Расширения Reflection API
Альтернативный вариант синтаксиса
Установка и использование компилятора
Требования
Структура каталогов
Сборка компилятора
Запуск компилятора

Экспериментальный компилятор – исходные файлы

Основная идея

В данной статье рассматривается расширение языка программирования Java, которое позволяет существенно сократить количество ошибок, связанных с разыменованием нулевого указателя и обычно проявляющихся в виде неожиданного исключения java.lang.NullPointerException. Для данного расширения в последствии планируется создать JSR (Java Specification Request).

При возникновении исключений java.lang.NullPointerException истинный дефект зачастую кроется там, где переменным присваивается нулевое значение. Чтобы найти ошибку, обычно необходимо обратно «прокручивать» выполнение программы от того момента, где возникло исключение, до того – где переменной был присвоен null. Этот поиск может занимать много времени и быть особенно неприятным, если ошибка была «запрограммирована» далеко от того места, где она проявилась.

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

Введение

Давайте представим, что мы разрабатываем сложную систему и используем класс Person, который, возможно, разработал кто-то другой. Для начала нам нужно просто создать экземпляр этого класса и вызвать у него метод printItself.

        package my;

import another.Person;

publicclass Main {
	privatestatic String extractFirstName(String[] args) {
		return (args.length > 0)? args[0]: null;
	}

	privatestatic String extractLastName(String[] args) {
		return (args.length > 1)? args[1]: null;
	}

	publicstaticvoid main(String[] args) {
		String firstName = extractFirstName(args);
		String lastName = extractLastName(args);
		Person person = new Person(firstName, lastName);
		person.printItself(); // line 18
	}
}

Причем, сам класс Person выглядит приблизительно так:

        package another;

publicclass Person {
	privatefinal String firstName;
	privatefinal String lastName;

	public Person(String firstName, String lastName) {
		this.firstName = firstName;
		this.lastName = lastName;
	}

	publicvoid printItself() {
		System.out.print(lastName.toUpperCase()); // line 13
		System.out.print(“, ”);
		System.out.print(firstName.toUpperCase());
		System.out.println();
	}
}

Откомпилировали, запустили, в результате получили:

        $ java my.Main foo bar
BAR, FOO

Вроде все нормально. Но! если мы запустим программу не c двумя параметрами коммандной строки, а с одним, то получим следующее:

        $ java my.Main foo
java.lang.NullPointerException
	at another.Person.printItself(Person.java:13)
	at my.Main.main(Main.java:18)
Exception in thread "main"

Возникла ошибка. Неприятно то, что, во-первых, ошибка возникла где-то в недрах чужого кода, а во-вторых, глядя на 18 строку нашего кода, понять причину ошибки сложно. Проанализировав код класса Person, выясняется следующее: экземпляр этого класса должен быть инициализирован ненулевыми значениями firstName и lastName. А мы в некоторых случаях передавали нулевые. Т.е. здесь имеет место недопонимание контракта между классом Person и кодом, его использующим. Впрочем, данная особенность класса Person могла бы быть и явно задокументирована в javadoc (или в каком-нибудь другом виде документации), а ошибка возникла скорее по невнимательности.

Прежде чем идти дальше, важно отметить два момента:

  1. Данная ошибка проявляется только при определенном стечении обстоятельств. Это означает, что она могла проявиться при эксплуатации системы, даже если система предварительно тестировалась.
  2. Ошибка возникает совсем не там, где она была «запрограммирована». В реальных условиях, создание класса Person и вызов метода printItself могут разделять тонны кода. Это означает, что между моментом, когда проявление ошибки может быть обнаружено, и моментом, когда будет найдено место, где ее нужно исправить, может пройти немало времени.

Итак, как же предотвратить подобные ошибки? Один из вариантов, – это найти более-менее адекватную замену для некорректных входных данных. Т.е. класс Person мог бы быть реализован так:

        package another;

publicclass Person {
	privatefinal String firstName;
	privatefinal String lastName;

	public Person(String firstName, String lastName) {
		this.firstName = (firstName != null)? firstName: “unknown”;
		this.lastName = (lastName != null)? lastName: “unknown”;
	}

	publicvoid printItself() {
		System.out.print(lastName.toUpperCase());
		System.out.print(“, ”);
		System.out.print(firstName.toUpperCase());
		System.out.println();
	}
}

В этом случае приложение не «ломается», а продолжает работать, хотя, возможно, и не совсем правильно. Достоинство данного подхода очевидно – повышается усточивость программы. А недостатки такие: во-первых, не всегда можно подобрать подходящую замену некорректным данным; во-вторых, такой подход «маскирует» ошибки, связанные с нарушениями контракта. Пользователь класса Person может даже и не подозревать, что он нарушает контракт, передавая в конструктор класса нулевые значения. И даже более того, он может завязаться на особенности данной реализации реагировать на некорректные аргументы. А это означает, что при смене на новую реализацию Person’а (которая полностью соответствует контракту, но несколько иначе реагирует на некорректные данные) очень высок риск того, что пользовательский код перестанет работать правильно.

Второй вариант – это всегда проверять корректность аргументов и в случае, если аргументы некорректны, выбрасывать соответствующее исключение.

        package another;

publicclass Person {
	privatefinal String firstName;
	privatefinal String lastName;

	public Person(String firstName, String lastName) {
		if(firstName == null)
			thrownew IllegalArgumentException(“firstName”);
		if(lastName == null)
			thrownew IllegalArgumentException(“lastName”);
		this.firstName = firstName;
		this.lastName = lastName;
	}

	publicvoid printItself() {
		System.out.print(lastName.toUpperCase());
		System.out.print(“, ”);
		System.out.print(firstName.toUpperCase());
		System.out.println();
	}
}

Этот подход хорош уже тем, что позволяет обнаруживать и диагностировать ошибки значительно раньше. Однако и данный подход не лишен недостатков: во-первых, для того, чтобы ошибка была обнаружена, все равно необходимо определенное стечение обстоятельств (как минимум, программа должна быть хотя бы раз запущена); во-вторых, разработчик класса может забыть сделать необходимые проверки во всех местах.

А что, если бы компилятор мог сам обнаруживать неправильное использование класса? Он ведь не позволяет передавать в конструктор класса Person в качестве аргументов целочисленные значения или значения любых других типов, отличных от java.lang.String! Почему бы ему не предотвращать попытки передачи нулевых значений там, где ожидаются ненулевые? Это могло бы выглядеть, например, так:

. . .
publicclass Person {
. . .
	public Person(notnull String firstName, notnull String lastName) {
		this.firstName = firstName;
		this.lastName = lastName;
	}
. . .
}

И при такой попытке использования:

Person person = new Person(null, lastName);

компилятор мог бы выдавать сообщение об ошибке, которое бы говорило о том, что нулевое значение в качестве первого параметра недопустимо. А такую попытку:

Person person = new Person(“foo”, “bar”);

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

Хорошо, а как должен был бы наш компилятор реагировать на такой код?:

String firstName = “foo”;
String lastName = “bar”;
Person person = new Person(firstName, lastName);

По идее, не имея никакой информации о том, могут ли переменные firstName и lastName принимать нулевые значения, компилятор вправе выругаться в строке 3 с сообщением о том, что firstName и lastName должны быть строго ненулевыми. Мы можем сообщить об этом компилятору следующим образом:

        notnull String firstName = “foo”;
notnull String lastName = “bar”;
Person person = new Person(firstName, lastName);

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

В целом, использование компилятором нулабилити позволит:

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

Ненулевые ссылочные значения

Как известно, в Java существуют две категории типов – это примитивные типы и ссылочные. Соответственно, существуют две категории значений, с которыми можно оперировать (хранить в переменных, передавать в качестве аргументов, возвращать в качестве результата из методов…): примитивные значения и ссылочные.

Существует также особый null тип, тип выражения null. Поскольку этот тип не имеет имени, нельзя объявить переменную этого типа или привести к null типу. Нулевая ссылка – единственное допустимое значение данного типа. Нулевая ссылка всегда приводится к любому ссылочному типу. На практике программист может игнорировать существование null типа и считать, что null – это просто особый литерал, который может быть любого ссылочного типа.

Ссылочное значение (ссылка) является либо указателем на объект, либо нулевой ссылкой, которая не указывает ни на какой объект.

ПРИМЕЧАНИЕ

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

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

Модификатор notnull

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

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

        public
        class Person {
	// переменные экземпляра firstName и lastName
// могут принимать только ненулевые значения
privatenotnull String firstName;
	privatenotnull String lastName;
	// . . .
}

Этот же модификатор, применённый к методу, говорит о том, что метод гарантированно возвращает ненулевое значение.

        public
        class Person {
	// . . .
// метод getFirstName всегда возвращает
// ненулевое значение
publicnotnull String getFirstName() {
		return firstName;
	}
	// . . .
}

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

        public
        class Person {
	// . . .
// параметры firstName и lastName обязаны быть ненулевыми
public Person(notnull String firstName, notnull String lastName) {
		this.firstName = firstName;
		this.lastName = lastName;
	}

	// параметр value обязан быть ненулевым
publicnotnull String changeFirstName(notnull String value) {
		// локальная ненулевая переменная previousnotnull String previous = firstName;
		firstName = value;
		return previous;
	}
	// . . .
}

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

        try {
	// . . .
}
catch(notnull IOException ioexc) {
	// ioexc является ненулевой переменной
}
catch(SAXException saxexc) {
	// saxexc также является ненулевой переменной
}

Модификатор notnull не может использоваться с примитивными типами, поскольку понятие нулабилити имеет смысл только по отношению к ссылочным значениям.

        // ошибка компиляции: int – примитивный тип

        public
        notnull
        int index;

// ошибка компиляции: void – примитивный тип
notnullvoid doSomething() { … }

Также модификатор notnull не может применяться к классам/интерфейсам и конструкторам.

        // ошибка компиляции: notnull не может применяться к классам

        public
        notnull
        class BadClass {
}

// ошибка компиляции: notnull не может применяться к интерфейсам
publicnotnullinterface BadInterface {
}

publicclass BadClassWithConstructor {
	// ошибка компиляции: notnull не может применяться к конструкторам
publicnotnull BadClassWithConstructor() { … }
}

Оператор приведения нулабилити

Попытка использовать обычное ссылочное значение там, где требуется ненулевое, вызывает ошибку компиляции. Возникновение данной ошибки чрезвычайно полезно, поскольку оно предотвращает использование потенциально опасных нулевых значений (собственно ради этого и создавалось рассматриваемое расширение языка). Однако если бы не существовало способа преобразовать обычные ссылки в ненулевые – использовать ненулевые ссылки было бы весьма неудобно. И такой способ существует. Обычные ссылки можно привести к ненулевым, если сделать это явно при помощи оператора приведения нулабилити. Выглядит это так:

        public
        class Sample {
	publicnotnull Integer i = new Integer(13);

	publicvoid m(Integer j) {
		// приводим обычную ссылку i к ненулевой
		i = (notnull) j;
	}

	publicnotnull Integer p() {
		Integer x = new Integer(44);
		// приводим обычную ссылку x к ненулевой
return (notnull) x;
	}

}

Такое преобразование требует проведения дополнительной проверки на этапе выполнения. Если проверяемое обычное ссылочное значение равно null, то возбуждается исключение java.lang.NullPointerException.

Предопределенные ненулевые ссылочные значения

Кроме таких ненулевых ссылочных выражений как: ненулевые переменные (т.е. которые объявлены с модификатором notnull); вызовы методов, возвращающих ненулевые значения (т.е. тех методов, которые тоже объявлены с notnull) и выражения с оператором приведения нулабилити – существуют также предопределенные ненулевые ссылочные значения. К таким выражениям относятся:

  1. строковые литералы;
  2. класс-литералы;
  3. this, и квалифицированный this (т.е. ссылка на экземпляр внешнего класса);
  4. оператор new.
        public
        class Sample {
	// строковый литералpublicnotnull String var = “sample”;
	// class литералpublicnotnull Class clazz = Sample.class;

	publicnotnull Object f() {
		// thisreturnthis;
	}

	publicstaticnotnull Object g(Sample obj) {
		// квалифицированное выражение создания экземпляра классаreturn obj.new Inner();
	}

	publicnotnullint[] h() {
		// выражение создания массиваreturnnewint[10];
	}

	publicclass Inner {
		publicnotnull Object x() {
			// квалифицированный thisreturn Sample.this;
		}

		publicnotnull Object y() {
			// выражение создания экземпляра классаreturnnew Sample();
		}
	}
}

Производные ненулевые ссылочные значения

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

  1. оператор приведения типа, если приводимое подвыражение является ненулевой ссылкой;
  2. условный оператор ‘?:’, если оба подвыражения-выбора являются ненулевыми ссылками;
  3. оператор присваивания (составной оператор присваивания), если самое правое подвыражение является ненулевой ссылкой.
        public
        class Sample {
	publicnotnull String castToString(notnull Object obj) {
		// приведение типа сохраняет notnullreturn (String) obj;
	}

	publicnotnull Integer max(notnull Integer obj1, notnull Integer obj2) {
		int i1 = obj1.intValue();
		int i2 = obj2.intValue();
		// условный оператор ?: сохраняет notnull,// если оба подвыражения ненулевыеreturn (i1>i2)? obj1: obj2;
	}

	publicnotnull Long makeLong(long l, Long[] array) {
		// оператор присваивания является ненулевым,// если самое правое выражение ненулевоеreturn (array[0] = new Long(l));
	}
}

Инициализация переменных

Ненулевые ссылочные переменные должны быть явно проинициализированы ненулевым значением. Это, в общем-то, понятно, поскольку то значение, которым инициализируются обычные ссылочные переменные по умолчанию (т.е. null), не подходит для ненулевых. В связи с этим существуют такие правила:

  1. локальные ненулевые переменные должны быть проинициализированы ненулевым ссылочным выражением при объявлении или до их первого использования по коду;
  2. статические ненулевые переменные (переменные класса) должны быть проинициализированы ненулевым ссылочным выражением при объявлении или в статическом инициализаторе;
  3. переменные экземпляра класса должны быть проинициализированы ненулевым ссылочным выражением при объявлении, в блоке инициализации экземпляра или во всех конструкторах.
        public
        class InitSample {

	publicvoid f() {
		// локальная переменная инициализируется при объявленииnotnull String a = “hello”;
		// локальная переменная инициализируется до первого использованияnotnull String b;
		b = a;
		// использование
		System.out.println(a);
		System.out.println(b);
	}

	// статическое поле инициализируется при объявленииprivatestaticnotnull String s0 = “s0”;

	// статическое поле инициализируется в статическом инициализатореprivatestaticnotnull String s1;
	static {
		s1 = “s1”;
	}

	// поле инициализируется при объявленииprivatenotnull String f0 = “f0”;

	// поле инициализируется в блоке инициализации экземпляраprivatenotnull String f1;
	{
		f1 = “f1”;
	}

	// поле инициализируется в конструктореprivatenotnull String f2;
	public InitSample() {
		f2 = “f2”;
	}

	// ошибка компиляции: статическое поле нигде не инициализируетсяprivatestaticnotnull String e0;

	// ошибка компиляции: поле нигде не инициализируетсяprivatenotnull String e1;
}

На этапе выполнения при попытке получить значение непроинициализированных ненулевых переменных должно возникать исключение java.lang.NullPointerException.

        public
        class UninitializedAccess {
	privatenotnull Integer v;

	UninitializedAccess() {
		bad();
		v = new Integer(13);
	}

	privatevoid bad() {
		// вот здесь происходит попытка взять еще// не проинициализированное значение поля v
		Integer x = v;
		// до следующей инструкции выполнение не дойдет, так как// предыдущая возбуждает NullPointerException
		System.out.println(x);
	}
}

Переопределение методов при наследовании

При расширении классов и/или реализации интерфейсов существует два правила, касающихся переопределения методов:

  1. если класс-наследник переопределяет метод, который в базовом классе (или интерфейсе) был с модификатором notnull, то в классе-наследнике переопределенный метод тоже должен быть с модификатором notnull.
  2. если класс-наследник переопределяет метод, который в базовом классе (или интерфейсе) содержал аргументы, которые не были помечены модификатором notnull, то в классе-наследнике соответствующие аргументы тоже должны оставаться без модификатора notnull.

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

Аналогично и с аргументами метода. Если метод базового класса обязуется принимать в качестве какого-то из параметров обычное ссылочное значение, то и в наследнике этот аргумент тоже должен принимать обычное значение. Поскольку иначе пользовательский код, работая с экземпляром класса-наследника через базовый интерфейс, мог бы послать в качестве данного ненулевого параметра нулевое значение, что недопустимо.

        public
        abstract
        class Base {
	publicabstractnotnull Object returnNotNull();
	publicabstract Object returnNullable();
	publicabstractvoid acceptNotNull(notnull Object o);
	publicabstractvoid acceptNullable(Object o);
}

        public
        class Derived extends Base {

	// ошибка компиляции: в базовом классе этот метод был объявлен как notnullpublic Object returnNotNull() {
		returnnull;
	}

	// все нормальноpublicnotnull Object returnNullable() {
		returnthis;
	}

	// все нормальноpublicvoid acceptNotNull(Object o) {
	}

	// ошибка компиляции: в базовом классе этот метод принимал обычное значение, а не ненулевоеpublicvoid acceptNullable(notnull Object o) {
	}

}

Перегрузка методов

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

        public
        class OverloadSample {

	publicvoid someMethod(notnull String s) {
	}

	// ошибка компиляции: такой метод уже существуетpublicvoid someMethod(String s) {
	}

}

Расширения Reflection API

Данное расширение языка коснется также и Reflection API, поскольку информация о том, является ли какое-то поле ненулевым, возвращает ли метод ненулевое значение и должен ли метод или коструктор принимать ненулевые значения в качестве каких-то параметров, может быть полезна на этапе выполнения.

Итак, в классе java.lang.reflect.Field добавляется новый метод isNotNull, который возвращает true, если данное поле является ненулевым, и false – если поле содержит обычное ссылочное значение. В том случае, если поле примитивного типа (byte, short, int, long, char, float, double или boolean), то метод isNotNull должен возвращать true.

Кроме того, немного изменилось поведение существующих методов. Так, метод set должен выбрасывать java.lang.NullPointerException, если поле является ненулевым, а передаваемое значение value равно null. Метод get также должен бросать java.lang.NullPointerException, если данное (ненулевое) поле еще не было проинициализированно.

        package java.lang.reflect;

publicclass Field {
	// …publicboolean isNotNull();
	publicvoid set(Object obj, Object value);
	public Object get(Object obj);
}

В классе java.lang.reflect.Method добавляется два дополнительных метода. Метод isReturnNotNull возвращает true, если метод возвращает ненулевое ссылочное значение. Если метод возвращает значение примитивного типа (исключая void), isReturnNotNull также должен вернуть true.

Метод getParameterNotNulls возвращает массив из boolean, каждый элемент которого соответствует аргументу метода. Порядок, количество элементов и соответветствие аргументам должны быть такими, как и у возвращаемого значения метода getParameterTypes(). Элемент массива boolean равен true, если соответствующий параметр обязан быть ненулевым ссылочным или примитивным значением.

Существующий метод invoke должен выбрасывать java.lang.NullPointerException, если для какого-либо ненулевого параметра был передан null.

        package java.lang.reflect;

publicclass Method {
	// …publicboolean isReturnNotNull();
	publicnotnullboolean[] getParameterNotNulls();
	public Object invoke(Object obj, Object[] args);
}

В классе java.lang.reflect.Constructor добавлется метод getParameterNotNulls, семантика которого такая же, как и одноименного метода в классе java.lang.reflect.Method.

Существующий метод newInstance должен выбрасывать java.lang.NullPointerException, если для какого-либо ненулевого параметра был передан null.

        package java.lang.reflect;

publicclass Constructor {
	// …publicnotnullboolean[] getParameterNotNulls();
	publicnotnull Object newInstance(Object[] args);
}

Альтернативный вариант синтаксиса

Основным недостатком приведенного выше способа внедрить идею notnull в Java является то, что он подразумевает расширение синтаксиса, а именно – добавление нового ключевого слова ‘notnull’. Это означает, что тот Java код, который использовал ‘notnull’ в качестве идентификатора и нормально компилировался старыми компиляторами, уже не будет компилироваться новым компилятором. Однако если воспользоваться некоторыми нововведениями в Java в версии 5, то можно реализовать идею notnull, не меняя язык.

ПРИМЕЧАНИЕ

На данный момент, использование аннотаций представляется более перспективным, чем расширение языка с добавлением нового ключевого слова.

Так, например, модификатор notnull можно заменить такой аннотацией java.lang.NotNull.

      package java.lang;

import java.lang.annotation.*;

@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target({
	ElementType.FIELD,
	ElementType.METHOD,
	ElementType.PARAMETER,
	ElementType.LOCAL_VARIABLE
})
public @interface NotNull {}

А использовать эту аннотацию можно так:

      public
      class Sample {
	private @NotNull String field;

	public Sample(@NotNull String field) {
		this.field = field;
	}

	public @NotNull String getField() {
		return field;
	}

	publicvoid setField(@NotNull String value) {
		field = value;
	}
}

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

Оператор же приведения нулабилити можно заменить полиморфным методом, таким, например, как narrowToNotNull (думаю, что он может по праву находиться в java.lang.Object):

      package java.lang;

publicclass Object {
	// . . .
	@SuppressWarnings(“unchecked”) // подавить предупреждение о небезопасном приведении типаpublicstaticfinal @NotNull <T> T narrowToNotNull(T value) {
		return (T) value.self();
	}

	private @NotNull Object self() {
		returnthis;
	}
}

Метод приведения нулабилити в данном примере выражен в терминах Java. Статический метод narrowToNotNull принимает обычную ссылку value и вызывает на ней приватный метод self() для того, чтобы из нее получить ненулевую ссылку (this – ненулевой по определению).

Метод narrowToNotNull может также быть выражен в нативном коде:

      package java.lang;

publicclass Object {
	// . . .publicstaticfinal native @NotNull <T> T narrowToNotNull(T value);
}

      /*
 * Class:     java_lang_Object
 * Method:    narrowToNotNull
 * Signature: (Ljava/lang/Object;)Ljava/lang/Object;
 */
JNIEXPORT jobject JNICALL Java_java_lang_Object_narrowToNotNull
	(JNIEnv *env, jclass clazz, jobject value)
{
	if(!value)
		env->ThrowNew(env, env->FindClass(env, “java/lang/NullPointerException”), “”);
	return value;
}

Использовать narrowToNotNull можно так:

      import Sample;

publicclass SampleUser {
	publicstaticvoid main(String[] args) {
		// использования метода narrowToNotNull:// конструктор класса Sample может принимать// только ненулевое значение в качестве параметра
		Sample sample = new Sample(narrowToNotNull(args[0]));
	}
}

Использование аннотаций позволило бы также не расширять Reflection API специально для получения информации о нулабилити полей и методов, а использовать для этих целей методы из java.lang.reflect.AnnotatedElement.

Установка и использование компилятора

В качестве Proof of Concept был разработан компилятор с поддержкой notnull расширения. За основу был взят компилятор из Eclipse 2.1.2 (http://www.eclipse.org/). В данном компиляторе используется подход на основе добавления нового ключевого слова ‘notnull’.

Требования

Для сборки комплятора требуются JDK (http://java.sun.com/j2se/, использовалась версия 1.4.2) и Apache Ant (http://ant.apache.org/, использовалась версия 1.5.1). Для unit тестирования необходим также JUnit (http://www.junit.org/, использовалась версия 3.8.1)

Структура каталогов

Компилятор с поддержкой notnull расширения поставляется в исходниках. Поставка представляет собой файл nn.jar. Чтобы распаковать nn.jar, достаточно выполнить в командной строке такую команду:

jar xf nn.jar

В распакованном виде поставка выглядит так:

/initial-compiler
	/sources
		. . .
	java_1_4.g
	build.properties
	build.xml
/notnull-compiler
	/sources
		. . .
	java_1_4_n.g
	build.properties
	build.xml
/notnull-compiler-tests
	/folders
		. . .
	/sources
		. . .
	build.properties
	build.xml
cpl-v10.html

Каталог notnull-compiler содержит исходники компилятора с notnull расширением. Файл java_1_4_n.g – грамматика языка. Файлы build.xml и build.properties – компиляционные скрипты для ant’а. В подкаталоге sources – собственно сами исходники.

Каталог initial-compiler содержит исходники компилятора, на основе которого был написан компилятор с notnull расширением. Они поставляются для того, чтобы можно было с ними сравнить исходники notnull компилятора.

Каталог notnull-compiler-tests содержит unit тесты для notnull компилятора.

Файл cpl-v10.html – “Common Public License - v 1.0”

Сборка компилятора

Для сборки компилятора необходимо зайти в каталог notnull-compiler (или initial-compiler) и выполнить команду

ant

В результате будут созданы каталоги classes (в котором будут находиться откомпилированные классы) и каталог results (в котором будет лежать .jar файл)

ПРИМЕЧАНИЕ

Несмотря на то, что в поставку входят файлы с грамматиками java_1_4.g и java_1_4_n.g, они непосредственно в сборке не учавствуют. Поэтому изменения, сделанные в них, на результат компиляции никак не влияют.

Для того чтобы запустить unit тесты notnull компилятора, необходимо (после компиляции notnull компилятора) выполнить команду ant в каталоге notnull-compiler-tests.

Запуск компилятора

После компиляции notnull компилятора в каталоге notnull-compiler/results появится файл javacnn.jar. Это и есть компилятор. Использовать его можно так:

java –jar javacnn.jar <опции командной строки>

Например,

java –jar javacnn.jar -help

выводит список всех доступных опций компилятора. А,

java –jar javacnn.jar test/*.java –d out

компилирует все .java-файлы в каталоге test и результат складывает в каталог out.


Любой из материалов, опубликованных на этом сервере, не может быть воспроизведен в какой бы то ни было форме и какими бы то ни было средствами без письменного разрешения владельцев авторских прав.
    Сообщений 68    Оценка 860 [+0/-2]         Оценить