Java Serialization 2013 — различия между версиями
Материал из SEWiki
Snurk (обсуждение | вклад) (→1. TestFrame) |
Snurk (обсуждение | вклад) (→Лекция о сериализации в Java.) |
||
(не показано 7 промежуточных версий этого же участника) | |||
Строка 4: | Строка 4: | ||
* [http://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html Java Object Serialization Specification] | * [http://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html Java Object Serialization Specification] | ||
− | == | + | == Материалы лекции == |
<source lang="java"> | <source lang="java"> | ||
− | + | /****Введение****/ | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | /** | |
+ | * | ||
+ | * Сериализация -- запись объекта в байтовый поток. | ||
+ | * | ||
+ | * Десереализация -- CO. | ||
+ | * | ||
+ | * Все в пакете java.io | ||
+ | * | ||
+ | * Мотивация: | ||
+ | * 1. dump (ex. JavaBeans persistence) | ||
+ | * 2. RMI | ||
+ | */ | ||
+ | class Point { | ||
+ | double x; | ||
+ | double y; | ||
+ | } | ||
+ | /** | ||
+ | * В простейшем случае достаточно добавить маркерный интерфейс implements | ||
+ | * Serializable | ||
+ | */ | ||
+ | @SuppressWarnings("serial") | ||
+ | class Point1 implements Serializable { | ||
+ | double x; | ||
+ | double y; | ||
+ | |||
+ | Point1(double x, double y) { | ||
+ | this.x = x; | ||
+ | this.y = y; | ||
+ | } | ||
+ | |||
+ | } | ||
+ | |||
+ | class SimplePointTest { | ||
+ | void SimpleWrite(OutputStream os) throws /*NotSerializableException,*/ IOException { | ||
+ | Point1 p = new Point1(10., 10.); | ||
+ | ObjectOutput out = new ObjectOutputStream(os); | ||
+ | out.writeObject(p); | ||
+ | os.flush(); | ||
+ | } | ||
+ | |||
+ | void SimpleRead(InputStream is) throws IOException, ClassNotFoundException { | ||
+ | ObjectInput in = new ObjectInputStream(is); | ||
+ | Point1 p = (Point1) in.readObject(); | ||
+ | System.out.println("x=" + p.x); | ||
+ | System.out.println("y=" + p.y); | ||
+ | } | ||
+ | |||
+ | @Test | ||
+ | public void SimpleTest() throws FileNotFoundException, IOException, | ||
+ | ClassNotFoundException { | ||
+ | try (OutputStream os = new FileOutputStream("point.ser")) { | ||
+ | SimpleWrite(os); | ||
+ | } | ||
+ | |||
+ | try (InputStream is = new FileInputStream("point.ser")) { | ||
+ | SimpleRead(is); | ||
+ | } | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /****Процесс сериализации****/ | ||
/** | /** | ||
− | * | + | * В сериализации участвуют ВСЕ достижимые объекты. |
+ | * | ||
+ | * Запись: | ||
+ | * 1. Запись предка (если он сериализуем) | ||
+ | * 2. Запись полей (если они сериализуемы) | ||
+ | * | ||
+ | * Циклические зависимости корректно обрабатываются автоматически. | ||
+ | * | ||
+ | * Чтение: | ||
+ | * 1. Выделение памяти под объект | ||
+ | * 2. Чтение предка | ||
+ | * 3. Чтение полей | ||
*/ | */ | ||
− | |||
− | |||
− | + | /** | |
− | + | * Все поля должны быть сериализуемы! | |
− | + | */ | |
− | + | @SuppressWarnings("serial") | |
− | + | class Polygon implements Serializable { | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | List<Point3> vertices = new ArrayList<>(); | |
− | + | ||
− | + | } | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | /** | |
− | + | * Пока все просто, но впечатление обманчиво! | |
− | + | * | |
+ | * 1. Внутреннее представление становится частью интерфейса | ||
+ | * Инкапсуляция летит к чертям! | ||
+ | * 2. Увеличивает объемы тестирования новых версий | ||
+ | * 3. Дополнительный конструктор! | ||
+ | * Необходимо обеспечивать все инварианты | ||
+ | * Некоторые дыры в безопасности | ||
+ | * 4. Все потомки становятся сериализуемыми! | ||
+ | */ | ||
+ | /****Управление сериализацией****/ | ||
− | + | /** | |
− | + | * Serial version UID (stream unique identifier) | |
− | + | * | |
− | + | * Применяется для обеспечения совместимости, когда версии класса изменяются | |
− | + | * | |
− | + | * По-умолчанию автоматически создается на основе имени, интерфейсов, | |
− | + | * public и protected полей | |
− | + | * | |
− | + | * В случае несовпадения -- InvalidClassException | |
− | + | * | |
+ | */ | ||
+ | class Point2 implements Serializable { | ||
+ | private static final long serialVersionUID = 4889340678034881968L; | ||
− | + | double x; | |
− | + | double y; | |
− | + | } | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | |||
− | + | /** | |
− | + | * Модификатор transient. | |
− | + | * Исключает поле из процесса сериализации. | |
− | + | * Примеры: | |
− | + | * 1. Избыточная информация (предподсчитанные значения) | |
− | + | * 2. Несериализуемые поля | |
− | + | * | |
− | + | */ | |
− | + | class Point3 implements Serializable { | |
− | + | private static final long serialVersionUID = 4889340678034881968L; | |
− | + | ||
− | + | ||
+ | double x; | ||
+ | double y; | ||
+ | transient int precomputedHash; | ||
+ | |||
+ | public int hashCode() { | ||
+ | return precomputedHash; | ||
+ | } | ||
+ | |||
+ | private int countHash() { | ||
+ | //some very involved procedure | ||
+ | return 0; | ||
+ | } | ||
+ | |||
+ | Point3(double x, double y) { | ||
+ | this.x = x; | ||
+ | this.y = y; | ||
+ | precomputedHash = countHash(); | ||
+ | } | ||
+ | } | ||
− | + | /** | |
− | + | * Настраиваемая сериализация | |
− | + | * Методы | |
− | + | * readObject(ObjectInputStream in) -- должен прочесть состояние из потока | |
− | + | * writeObject(ObjectOutputStream out) -- должен записать состояние в поток | |
− | + | * | |
− | + | * Процесс чтения | |
− | + | * 1. Выделение памяти | |
− | + | * 2. Вызов readObject | |
− | + | * | |
− | + | */ | |
− | + | class Point4 implements Serializable { | |
− | + | private static final long serialVersionUID = 4889340678034881968L; | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | double x; | |
− | + | double y; | |
− | + | ||
− | + | transient int precomputedHash; | |
− | + | ||
− | + | public int hashCode() { | |
− | + | return precomputedHash; | |
− | + | } | |
− | + | ||
− | + | private int countHash() { | |
− | + | //some very involved procedure | |
− | + | return 0; | |
− | + | } | |
− | + | ||
− | + | Point4(double x, double y) { | |
− | + | this.x = x; | |
− | + | this.y = y; | |
− | + | precomputedHash = countHash(); | |
− | + | } | |
− | + | ||
− | + | /* | |
+ | * Отвечает только за запись полей ЭТОГО класса, не предка и не потомка! | ||
+ | */ | ||
+ | private void writeObject(ObjectOutputStream s) throws IOException { | ||
+ | /* | ||
+ | * Реализует стандартный механизм сериализации. | ||
+ | * Может вызываться только из writeObject, иначе NotActiveException | ||
+ | */ | ||
+ | s.defaultWriteObject(); | ||
+ | } | ||
− | + | //отвечает только за чтение полей ЭТОГО класса, не предка и не потомка! | |
− | + | private void readObject(ObjectInputStream s) throws IOException, | |
+ | ClassNotFoundException { | ||
+ | /* | ||
+ | * Реализует стандартный механизм десериализации. | ||
+ | * Может вызываться только из readObject, иначе NotActiveException | ||
+ | */ | ||
+ | s.defaultReadObject(); | ||
+ | //восстановление инвариантов | ||
+ | precomputedHash = countHash(); | ||
+ | } | ||
+ | } | ||
− | + | /** | |
− | + | * Что если внутреннее представление класса изменилось? | |
− | + | * | |
− | + | * Ручная сериализация полей | |
− | + | * | |
− | + | * Константа ObjectStreamField[] serialPersistentFields -- сериализуемые поля | |
− | + | * ObjectOutputStream.PutField putFields() -- поля для записи | |
− | + | * | |
− | + | * writeFields записывает поля | |
− | + | * | |
− | + | * ObjectInputStream.GetField readFields() -- чтение полей | |
− | + | * | |
− | + | */ | |
− | + | class Point5 implements Serializable { | |
− | + | private static final long serialVersionUID = 4889340678034881968L; | |
− | + | ||
− | + | ||
− | + | double angle; | |
− | + | double rad; | |
− | + | ||
− | + | private static final ObjectStreamField[] serialPersistentFields = { | |
− | + | new ObjectStreamField("x", double.class), | |
− | + | new ObjectStreamField("y", double.class) }; | |
− | + | ||
− | + | private void writeObject(ObjectOutputStream s) throws IOException { | |
− | + | ObjectOutputStream.PutField fields = s.putFields(); | |
− | + | fields.put("x", Math.cos(angle) * rad); | |
− | + | fields.put("y", Math.sin(angle) * rad); | |
− | + | s.writeFields(); | |
− | + | } | |
− | + | ||
− | + | private void readObject(ObjectInputStream s) throws IOException, | |
− | + | ClassNotFoundException { | |
− | + | ObjectInputStream.GetField fields = s.readFields(); | |
− | + | double x = (double) fields.get("x", 0.); | |
− | + | double y = (double) fields.get("y", 0.); | |
− | + | // count angle and mod here | |
− | + | // angle = ... | |
− | + | // rad = ... | |
− | + | } | |
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
− | + | ||
} | } | ||
+ | |||
+ | /** | ||
+ | * Сериализация с несериализуемым предком. | ||
+ | * | ||
+ | * Если хочется отнаследовать сериализуемый класс от | ||
+ | * несериализуемого предка, то могут быть проблемы... | ||
+ | * | ||
+ | * Формальное требование одно -- предок должен иметь | ||
+ | * конструктор по-умолчанию. | ||
+ | * | ||
+ | * Но только ли?! | ||
+ | */ | ||
+ | class NonSerializablePoint { | ||
+ | private double x; | ||
+ | private double y; | ||
+ | |||
+ | protected double getX() {return x;} | ||
+ | |||
+ | protected double getY() {return y;} | ||
+ | |||
+ | protected void init(double x, double y) { | ||
+ | this.x = x; | ||
+ | this.y = y; | ||
+ | } | ||
+ | |||
+ | protected NonSerializablePoint() {} | ||
+ | |||
+ | public NonSerializablePoint(double x, double y) { | ||
+ | init(x, y); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | // Serializable subclass of nonserializable stateful class | ||
+ | class ColoredPoint extends NonSerializablePoint implements Serializable { | ||
+ | private int color; | ||
+ | |||
+ | // Конструктор использует обычный механизм | ||
+ | public ColoredPoint(double x, double y, int color) { | ||
+ | super(x, y); | ||
+ | this.color = color; | ||
+ | } | ||
+ | |||
+ | private void readObject(ObjectInputStream s) throws IOException, | ||
+ | ClassNotFoundException { | ||
+ | s.defaultReadObject(); | ||
+ | // Manually deserialize and initialize superclass state | ||
+ | double x = s.readDouble(); | ||
+ | double y = s.readDouble(); | ||
+ | init(x, y); | ||
+ | } | ||
+ | |||
+ | private void writeObject(ObjectOutputStream s) throws IOException { | ||
+ | s.defaultWriteObject(); | ||
+ | // Manually serialize superclass state | ||
+ | s.writeDouble(getX()); | ||
+ | s.writeDouble(getY()); | ||
+ | } | ||
+ | |||
+ | private static final long serialVersionUID = 1856835860954L; | ||
+ | } | ||
+ | |||
+ | |||
+ | |||
+ | /** | ||
+ | * Externalizable | ||
+ | * | ||
+ | * Сериализация "в ручную" | ||
+ | * | ||
+ | * void readExternal(ObjectInput in) | ||
+ | * void writeExternal(ObjectOutput out) | ||
+ | * | ||
+ | * Должен быть конструктор по-умолчанию | ||
+ | * | ||
+ | * Чтение: вызов конструктора по-умолчанию, вызов readExternal | ||
+ | */ | ||
+ | class Point6 implements Externalizable { | ||
+ | private double x; | ||
+ | private double y; | ||
+ | |||
+ | Point6(double x, double y) { | ||
+ | this.x = x; | ||
+ | this.y = y; | ||
+ | } | ||
+ | |||
+ | @Override | ||
+ | public void writeExternal(ObjectOutput out) throws IOException { | ||
+ | out.writeDouble(x); | ||
+ | out.writeDouble(y); | ||
+ | } | ||
+ | @Override | ||
+ | public void readExternal(ObjectInput in) throws IOException, | ||
+ | ClassNotFoundException { | ||
+ | this.x = in.readDouble(); | ||
+ | this.y = in.readDouble(); | ||
+ | } | ||
+ | } | ||
+ | |||
+ | /** | ||
+ | * "Подмена" сериализуемого объекта. | ||
+ | * | ||
+ | * Когда вместо самого объекта сериализуется его альтернативное представление | ||
+ | * в виде другого объекта некоторого (возможно того же самого) сериализуемого класса. | ||
+ | * | ||
+ | * Object writeReplace() -- какой объект записать вместо данного | ||
+ | * Object readResolve() -- какой объект восстановить вместо прочитанного | ||
+ | * | ||
+ | * Реализуются у разных классов!!! | ||
+ | */ | ||
+ | |||
</source> | </source> |
Текущая версия на 16:21, 22 мая 2013
Что почитать
- "Effective Java", Joshua Bloch, Chapter 11
- Java Object Serialization Specification
Материалы лекции
/****Введение****/
/**
*
* Сериализация -- запись объекта в байтовый поток.
*
* Десереализация -- CO.
*
* Все в пакете java.io
*
* Мотивация:
* 1. dump (ex. JavaBeans persistence)
* 2. RMI
*/
class Point {
double x;
double y;
}
/**
* В простейшем случае достаточно добавить маркерный интерфейс implements
* Serializable
*/
@SuppressWarnings("serial")
class Point1 implements Serializable {
double x;
double y;
Point1(double x, double y) {
this.x = x;
this.y = y;
}
}
class SimplePointTest {
void SimpleWrite(OutputStream os) throws /*NotSerializableException,*/ IOException {
Point1 p = new Point1(10., 10.);
ObjectOutput out = new ObjectOutputStream(os);
out.writeObject(p);
os.flush();
}
void SimpleRead(InputStream is) throws IOException, ClassNotFoundException {
ObjectInput in = new ObjectInputStream(is);
Point1 p = (Point1) in.readObject();
System.out.println("x=" + p.x);
System.out.println("y=" + p.y);
}
@Test
public void SimpleTest() throws FileNotFoundException, IOException,
ClassNotFoundException {
try (OutputStream os = new FileOutputStream("point.ser")) {
SimpleWrite(os);
}
try (InputStream is = new FileInputStream("point.ser")) {
SimpleRead(is);
}
}
}
/****Процесс сериализации****/
/**
* В сериализации участвуют ВСЕ достижимые объекты.
*
* Запись:
* 1. Запись предка (если он сериализуем)
* 2. Запись полей (если они сериализуемы)
*
* Циклические зависимости корректно обрабатываются автоматически.
*
* Чтение:
* 1. Выделение памяти под объект
* 2. Чтение предка
* 3. Чтение полей
*/
/**
* Все поля должны быть сериализуемы!
*/
@SuppressWarnings("serial")
class Polygon implements Serializable {
List<Point3> vertices = new ArrayList<>();
}
/**
* Пока все просто, но впечатление обманчиво!
*
* 1. Внутреннее представление становится частью интерфейса
* Инкапсуляция летит к чертям!
* 2. Увеличивает объемы тестирования новых версий
* 3. Дополнительный конструктор!
* Необходимо обеспечивать все инварианты
* Некоторые дыры в безопасности
* 4. Все потомки становятся сериализуемыми!
*/
/****Управление сериализацией****/
/**
* Serial version UID (stream unique identifier)
*
* Применяется для обеспечения совместимости, когда версии класса изменяются
*
* По-умолчанию автоматически создается на основе имени, интерфейсов,
* public и protected полей
*
* В случае несовпадения -- InvalidClassException
*
*/
class Point2 implements Serializable {
private static final long serialVersionUID = 4889340678034881968L;
double x;
double y;
}
/**
* Модификатор transient.
* Исключает поле из процесса сериализации.
* Примеры:
* 1. Избыточная информация (предподсчитанные значения)
* 2. Несериализуемые поля
*
*/
class Point3 implements Serializable {
private static final long serialVersionUID = 4889340678034881968L;
double x;
double y;
transient int precomputedHash;
public int hashCode() {
return precomputedHash;
}
private int countHash() {
//some very involved procedure
return 0;
}
Point3(double x, double y) {
this.x = x;
this.y = y;
precomputedHash = countHash();
}
}
/**
* Настраиваемая сериализация
* Методы
* readObject(ObjectInputStream in) -- должен прочесть состояние из потока
* writeObject(ObjectOutputStream out) -- должен записать состояние в поток
*
* Процесс чтения
* 1. Выделение памяти
* 2. Вызов readObject
*
*/
class Point4 implements Serializable {
private static final long serialVersionUID = 4889340678034881968L;
double x;
double y;
transient int precomputedHash;
public int hashCode() {
return precomputedHash;
}
private int countHash() {
//some very involved procedure
return 0;
}
Point4(double x, double y) {
this.x = x;
this.y = y;
precomputedHash = countHash();
}
/*
* Отвечает только за запись полей ЭТОГО класса, не предка и не потомка!
*/
private void writeObject(ObjectOutputStream s) throws IOException {
/*
* Реализует стандартный механизм сериализации.
* Может вызываться только из writeObject, иначе NotActiveException
*/
s.defaultWriteObject();
}
//отвечает только за чтение полей ЭТОГО класса, не предка и не потомка!
private void readObject(ObjectInputStream s) throws IOException,
ClassNotFoundException {
/*
* Реализует стандартный механизм десериализации.
* Может вызываться только из readObject, иначе NotActiveException
*/
s.defaultReadObject();
//восстановление инвариантов
precomputedHash = countHash();
}
}
/**
* Что если внутреннее представление класса изменилось?
*
* Ручная сериализация полей
*
* Константа ObjectStreamField[] serialPersistentFields -- сериализуемые поля
* ObjectOutputStream.PutField putFields() -- поля для записи
*
* writeFields записывает поля
*
* ObjectInputStream.GetField readFields() -- чтение полей
*
*/
class Point5 implements Serializable {
private static final long serialVersionUID = 4889340678034881968L;
double angle;
double rad;
private static final ObjectStreamField[] serialPersistentFields = {
new ObjectStreamField("x", double.class),
new ObjectStreamField("y", double.class) };
private void writeObject(ObjectOutputStream s) throws IOException {
ObjectOutputStream.PutField fields = s.putFields();
fields.put("x", Math.cos(angle) * rad);
fields.put("y", Math.sin(angle) * rad);
s.writeFields();
}
private void readObject(ObjectInputStream s) throws IOException,
ClassNotFoundException {
ObjectInputStream.GetField fields = s.readFields();
double x = (double) fields.get("x", 0.);
double y = (double) fields.get("y", 0.);
// count angle and mod here
// angle = ...
// rad = ...
}
}
/**
* Сериализация с несериализуемым предком.
*
* Если хочется отнаследовать сериализуемый класс от
* несериализуемого предка, то могут быть проблемы...
*
* Формальное требование одно -- предок должен иметь
* конструктор по-умолчанию.
*
* Но только ли?!
*/
class NonSerializablePoint {
private double x;
private double y;
protected double getX() {return x;}
protected double getY() {return y;}
protected void init(double x, double y) {
this.x = x;
this.y = y;
}
protected NonSerializablePoint() {}
public NonSerializablePoint(double x, double y) {
init(x, y);
}
}
// Serializable subclass of nonserializable stateful class
class ColoredPoint extends NonSerializablePoint implements Serializable {
private int color;
// Конструктор использует обычный механизм
public ColoredPoint(double x, double y, int color) {
super(x, y);
this.color = color;
}
private void readObject(ObjectInputStream s) throws IOException,
ClassNotFoundException {
s.defaultReadObject();
// Manually deserialize and initialize superclass state
double x = s.readDouble();
double y = s.readDouble();
init(x, y);
}
private void writeObject(ObjectOutputStream s) throws IOException {
s.defaultWriteObject();
// Manually serialize superclass state
s.writeDouble(getX());
s.writeDouble(getY());
}
private static final long serialVersionUID = 1856835860954L;
}
/**
* Externalizable
*
* Сериализация "в ручную"
*
* void readExternal(ObjectInput in)
* void writeExternal(ObjectOutput out)
*
* Должен быть конструктор по-умолчанию
*
* Чтение: вызов конструктора по-умолчанию, вызов readExternal
*/
class Point6 implements Externalizable {
private double x;
private double y;
Point6(double x, double y) {
this.x = x;
this.y = y;
}
@Override
public void writeExternal(ObjectOutput out) throws IOException {
out.writeDouble(x);
out.writeDouble(y);
}
@Override
public void readExternal(ObjectInput in) throws IOException,
ClassNotFoundException {
this.x = in.readDouble();
this.y = in.readDouble();
}
}
/**
* "Подмена" сериализуемого объекта.
*
* Когда вместо самого объекта сериализуется его альтернативное представление
* в виде другого объекта некоторого (возможно того же самого) сериализуемого класса.
*
* Object writeReplace() -- какой объект записать вместо данного
* Object readResolve() -- какой объект восстановить вместо прочитанного
*
* Реализуются у разных классов!!!
*/