Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion lab-01-expr-calc/task.md
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ public interface Expression {
```
Каждая реализация интерфейса `Expression` будет звать у `visitor` метод, соответствующий своему типу, например, реализация `BinaryExpression` будет звать `visitBinaryExpression(this)`, и так далее.
Такой подход схож с первым вариантом через `instanceof`+cast, но более устойчив к ошибкам, и весь код обхода или "посещения" пишется один раз.
Таким образом, при добавлении нового класса (интерфейса) в иерархию мы будем вынуждены реализовать там метод `accept`, где будем вынуждены вызвать новый метод визитора (который тут же и добавим) и далее будем вынуждены реализовать этот метод во всех своих реализациях визитора. Так мы будем в каждый момент времени уверены, что наша кодовая базада поддерживает все существующие типы в конкретной "посещаемой" иерархии.
Таким образом, при добавлении нового класса (интерфейса) в иерархию мы будем вынуждены реализовать там метод `accept`, где будем вынуждены вызвать новый метод визитора (который тут же и добавим) и далее будем вынуждены реализовать этот метод во всех своих реализациях визитора. Так мы будем в каждый момент времени уверены, что наша кодовая база поддерживает все существующие типы в конкретной "посещаемой" иерархии.

В рамках нашего примера у нас получится так:
```java
Expand Down Expand Up @@ -251,3 +251,18 @@ public ToStringVisitor implements ExpressionVisitor {

- По всем вопросам к классам стандартной библиотеки смотрите их java-doc и пользуйтесь автокомплитом в IDE.
Так же очень полезно пользоваться stack-overflow и прочими ресурсами. Если не нашли сами ответ на любой вопрос - спрашивайте в телеграмм-чате курса, постараюсь вам помочь.

## Update #1

**Доп задания**:

_(Если сдали основное задание, то сделайте отдельный PR c реализацией дополнительных.
Если ещё не сдали - можно делать всё сразу)_

1. Так как прошли обобщения (generics), можно не возвращать тип `Object` из метода `accept` посетителя. Нужно сделать интерфейс
`ExpressionVisitor` обобщённым, и использовать тип-параметр как результат. Метод `accept` нужно, соответственно, так же
сделать обобщённым по возвращаемому значению и принимать визитор с нужным типом-параметром и нужной вариантностью*.
Это избавит нас от явных ручных преобразований типов.

---
* смотри [variance.md](variance.md).
74 changes: 74 additions & 0 deletions lab-01-expr-calc/variance.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
## Вариантность обобщённых типов.

Обобщённый тип `Generics<T>` называется
1. **ковариантным по `T`**, если из "`Derived` является подтипом `Base`" следует,
что `Generic<Derived>` является подтипом `Generic<Base>`.
2. **контравариантными по `T`**, если из "`Derived` является подтипом `Base`" следует,
что `Generic<Base>` является подтипом `Generic<Derived>`.
3. **инвариантным по `T`**, если `Generic<Derived>` и `Generic<Base>` не состоят ни в каком родстве.

## Вариантность в Java

- Java обобщённые типы по-умолчанию _инвариантны_ по всем своим параметрам типа. Но в Java есть механизм обозначения
вариантности по месту использования (use-site variance).
Рассмотрим пример:
```java
interface Holder<T> {
void consume(T object);
T produce();
}

// ...

void useHolderValue(Holder<? extends Number> producer) { // (1)
producer.produce(); // ok
// producer.consume(228) compiler error - "consumer case"
}
void populateHolder(Holder<? super Number> consumer) { // (2)
consumer.consume(2); // ok
consumer.consume(4L); // ok
consumer.consume(1.0); // ok
// Number n = consumer.produce(); // compiler error - "producer case"
}

void randomlyUseHolder(Holder<Number> holder) { // (3)
consumer.consume(2); // ok
consumer.consume(4L); // ok
consumer.consume(1.0); // ok
Number n = consumer.produce(); // ok
}

// ...

void demo() {
Holder<Integer> intHolder = new HolderImpl<>();
useHolderValue(intHolder); // ok - covariant, Integer extends Number => Holder<Integer> 'extends' Holder<Number>
// populateHolder(intHolder); // fail, accept only Holder<? super Number>, Integer isn't a superclass of Number.
// randomlyUseHolder(intHolder); // fail, invariant parameter, accepts only exactly Holder<Number>.

Holder<Number> numberHolder = new HolderImpl<>();
useHolderValue(intHolder); // ok - exact type
populateHolder(intHolder); // ok - exact type
randomlyUseHolder(intHolder); // ok - exact type

Holder<Serializable> serializableHolder = new HolderImpl<>();
// useHolderValue(intHolder); // fail - exact type
populateHolder(intHolder); // ok - contravariant, Serializable is a supertype of Number.
// randomlyUseHolder(intHolder); // fail, invariant parameter, accepts only exactly Holder<Number>.
}
```
1. `Holder<? extends Number>` тип - ковариантен по T - Holder<Integer> подойдёт как Holder<Number>.
Для таких объектов компилятором разрешено только читать из объекта.
2. `Holder<? super Number` тип - контравариантен по T - Holder<Serializable> подойдет как Holder<Number>.
Для таких объектов разрешено только записывать в объект.
3. `Holder<Number>` тип - инвариантен по T. Подходит только точно Holder<Number>.
Разрешено, очевидно, всё.

Существует мнемоника **PECS** - **P**roducer - **E**xtends, **C**onsumer - **S**uper.
- Если из объекта будут только читать - producer - можно использовать ковариантность (? extends ..)
- Если в объект будут только писать - consumer - можно использовать контравариантность (? super ..)
- Если нужно и то, и другое (по-умолчанию) - тогда тип инвариантен (собственно, по-умолчанию)

_Заметка._ встроенные Массивы в Java всегда _ковариантны_ по типу элементов.
Это в общем случае ошибочно, так что нужно быть аккуратным при
передаче массивов в методы, которые могут модифицировать массив-параметр.
17 changes: 17 additions & 0 deletions lab-02-dependency-injection/build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
plugins {
id 'java-library'
}

repositories {
mavenCentral()
}

dependencies {
api 'javax.inject:javax.inject:1'
testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
}

test {
useJUnitPlatform()
}
1 change: 1 addition & 0 deletions lab-02-dependency-injection/settings.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
rootProject.name = 'lab-02-dependency-injection'
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com._30something.DI;

public class ClassRegistrationException extends Exception {
public ClassRegistrationException(String message) {
super(message);
}
}
117 changes: 117 additions & 0 deletions lab-02-dependency-injection/src/main/java/com/_30something/DI/DI.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package com._30something.DI;

import java.security.AccessControlException;

import javax.inject.Inject;
import javax.inject.Singleton;
import java.lang.reflect.*;
import java.util.*;

public class DI {
private boolean registrationCompleted = false;
private final HashMap<Class<?>, Class<?>> associatedImplementations = new HashMap<>();
private final HashMap<Class<?>, Constructor<?>> associatedConstructors = new HashMap<>();
private final HashMap<Class<?>, Object> singletonsInstances = new HashMap<>();

public void registerClass(Class<?> newClass) throws InterfaceRegistrationException, ClassRegistrationException {
if (registrationCompleted) {
throw new AccessControlException("Registration completed for current DI");
}
if (newClass.isInterface()) {
throw new InterfaceRegistrationException("Interface registered without implementation");
}
if (associatedConstructors.containsKey(newClass)) {
throw new ClassRegistrationException("Double class registration");
}
List<Constructor<?>> constructors_list = Arrays.stream(newClass.getDeclaredConstructors()).toList();
int injectedConstructorsCounter = 0;
Constructor<?> supposedConstructor = null;
for (Constructor<?> constructor : constructors_list) {
if (constructor.isAnnotationPresent(Inject.class)) {
injectedConstructorsCounter++;
supposedConstructor = constructor;
}
}
if (injectedConstructorsCounter == 0) {
throw new ClassRegistrationException("Injected constructor of " + newClass + " not found");
}
if (injectedConstructorsCounter > 1) {
throw new ClassRegistrationException("Multiple injected constructors found in " + newClass);
}
if (!newClass.isAnnotationPresent(Singleton.class) &&
!Objects.equals(Modifier.toString(supposedConstructor.getModifiers()), "public")) {
throw new ClassRegistrationException("Supposed constructor of " + newClass + " must be public only");
}
associatedConstructors.put(newClass, supposedConstructor);
}

public void registerClass(Class<?> newInterface, Class<?> newImplementation)
throws InterfaceRegistrationException, ClassRegistrationException {
if (registrationCompleted) {
throw new AccessControlException("Registration completed for current DI");
}
if (newImplementation.isInterface()) {
throw new InterfaceRegistrationException("Attempt to register interface as implementation");
}
if (!newInterface.isInterface()) {
throw new InterfaceRegistrationException("Attempt to register implementation for non-interface class");
}
if (associatedImplementations.containsKey(newInterface)) {
throw new InterfaceRegistrationException("Attempt to register new implementation for interface");
}
if (!Arrays.stream(newImplementation.getInterfaces()).toList().contains(newInterface)) {
throw new InterfaceRegistrationException("Implementation doesn't correspond to interface");
}
if (!associatedConstructors.containsKey(newImplementation)) {
registerClass(newImplementation);
}
associatedImplementations.put(newInterface, newImplementation);
}

public void completeRegistration() throws ClassRegistrationException {
if (registrationCompleted) {
return;
}
for (Constructor<?> constructor : associatedConstructors.values()) {
for (Parameter parameter : constructor.getParameters()) {
if (!associatedConstructors.containsKey(parameter.getType()) &&
!associatedImplementations.containsKey(parameter.getType())) {
throw new ClassRegistrationException(
"Arguments of injected constructor " + constructor + " aren't registered");
}
}
if (!constructor.isAnnotationPresent(Inject.class)) {
throw new ClassRegistrationException("Constructor " + constructor + " must be marked with @Inject");
}
}
registrationCompleted = true;
}

public <T> T resolveClass(Class<T> newClass) throws ClassNotFoundException, InvocationTargetException,
InstantiationException, IllegalAccessException {
if (!registrationCompleted) {
throw new AccessControlException("Registration isn't completed for current DI");
}
if (!associatedConstructors.containsKey(newClass) && !associatedImplementations.containsKey(newClass)) {
throw new ClassNotFoundException("Requested class not found");
}
if (newClass.isInterface()) {
Class<?> implementation = associatedImplementations.get(newClass);
return newClass.cast(resolveClass(implementation));
}
if (singletonsInstances.containsKey(newClass)) {
return newClass.cast(singletonsInstances.get(newClass));
}
ArrayList<Object> createdInstances = new ArrayList<>();
Constructor<?> constructor = associatedConstructors.get(newClass);
for (Parameter parameter : constructor.getParameters()) {
createdInstances.add(resolveClass(parameter.getType()));
}
constructor.setAccessible(true);
T newInstance = newClass.cast(constructor.newInstance(createdInstances.toArray()));
if (newClass.isAnnotationPresent(Singleton.class)) {
singletonsInstances.put(newClass, newInstance);
}
return newInstance;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com._30something.DI;

public class InterfaceRegistrationException extends Exception {
public InterfaceRegistrationException(String message) {
super(message);
}
}
Loading