diff --git a/README.md b/README.md index 87ffcbb..1a6a1a9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Review Assignment Due Date](https://classroom.github.com/assets/deadline-readme-button-22041afd0340ce965d47ae6ef1cefeee28c7c493a6346c4f15d667ab976d596c.svg)](https://classroom.github.com/a/4ISSpVK4) **Java reflection** Ваша задача --- написать генератор экземпляров произвольных классов. diff --git a/src/main/java/org/example/GenerateExample.java b/src/main/java/org/example/GenerateExample.java index 47679a9..d10b5eb 100644 --- a/src/main/java/org/example/GenerateExample.java +++ b/src/main/java/org/example/GenerateExample.java @@ -1,15 +1,24 @@ package org.example; - -import org.example.classes.Example; +import org.example.classes.*; import org.example.generator.Generator; public class GenerateExample { public static void main(String[] args) { - var gen = new Generator(); + var gen = new Generator(3); try { - Object generated = gen.generateValueOfType(Example.class); - System.out.println(generated); + System.out.println(gen.generateValueOfType(Example.class)); + + Shape shape = gen.generateValueOfType(Shape.class); + System.out.println("shape: " + shape.getClass().getSimpleName() + + " area=" + shape.getArea() + " per=" + shape.getPerimeter()); + + Cart cart = gen.generateValueOfType(Cart.class); + System.out.println("cart items: " + (cart.getItems() == null ? 0 : cart.getItems().size())); + + BinaryTreeNode root = gen.generateValueOfType(BinaryTreeNode.class); + System.out.println("tree root data: " + (root == null ? null : root.getData())); + } catch (Throwable e) { throw new RuntimeException(e); } diff --git a/src/main/java/org/example/classes/BinaryTreeNode.java b/src/main/java/org/example/classes/BinaryTreeNode.java index 046ff56..52a9157 100644 --- a/src/main/java/org/example/classes/BinaryTreeNode.java +++ b/src/main/java/org/example/classes/BinaryTreeNode.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.generator.Generatable; + +@Generatable public class BinaryTreeNode { private Integer data; private BinaryTreeNode left; diff --git a/src/main/java/org/example/classes/Cart.java b/src/main/java/org/example/classes/Cart.java index 965237d..3b9e8dd 100644 --- a/src/main/java/org/example/classes/Cart.java +++ b/src/main/java/org/example/classes/Cart.java @@ -1,7 +1,10 @@ package org.example.classes; +import org.example.generator.Generatable; + import java.util.List; +@Generatable public class Cart { private List items; diff --git a/src/main/java/org/example/classes/Example.java b/src/main/java/org/example/classes/Example.java index eac9463..e0c8d1c 100644 --- a/src/main/java/org/example/classes/Example.java +++ b/src/main/java/org/example/classes/Example.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.generator.Generatable; + +@Generatable public class Example { int i; diff --git a/src/main/java/org/example/classes/Product.java b/src/main/java/org/example/classes/Product.java index e7dcc89..ba7aa89 100644 --- a/src/main/java/org/example/classes/Product.java +++ b/src/main/java/org/example/classes/Product.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.generator.Generatable; + +@Generatable public class Product { private String name; private double price; diff --git a/src/main/java/org/example/classes/Rectangle.java b/src/main/java/org/example/classes/Rectangle.java index 90b0886..5f6b174 100644 --- a/src/main/java/org/example/classes/Rectangle.java +++ b/src/main/java/org/example/classes/Rectangle.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.generator.Generatable; + +@Generatable public class Rectangle implements Shape { private double length; private double width; diff --git a/src/main/java/org/example/classes/Shape.java b/src/main/java/org/example/classes/Shape.java index c20a851..a7d1c1c 100644 --- a/src/main/java/org/example/classes/Shape.java +++ b/src/main/java/org/example/classes/Shape.java @@ -1,6 +1,9 @@ package org.example.classes; +import org.example.generator.Generatable; + +@Generatable(implementations = { Rectangle.class, Triangle.class }) public interface Shape { double getArea(); double getPerimeter(); -} \ No newline at end of file +} diff --git a/src/main/java/org/example/classes/Triangle.java b/src/main/java/org/example/classes/Triangle.java index 011e96f..e2c4c5a 100644 --- a/src/main/java/org/example/classes/Triangle.java +++ b/src/main/java/org/example/classes/Triangle.java @@ -1,5 +1,8 @@ package org.example.classes; +import org.example.generator.Generatable; + +@Generatable public class Triangle implements Shape { private double sideA; private double sideB; diff --git a/src/main/java/org/example/generator/Generatable.java b/src/main/java/org/example/generator/Generatable.java new file mode 100644 index 0000000..cb8d4c0 --- /dev/null +++ b/src/main/java/org/example/generator/Generatable.java @@ -0,0 +1,10 @@ +package org.example.generator; + +import java.lang.annotation.*; + +@Retention(RetentionPolicy.RUNTIME) +@Target(ElementType.TYPE) +public @interface Generatable { + Class[] implementations() default {}; + int maxDepth() default 0; +} diff --git a/src/main/java/org/example/generator/Generator.java b/src/main/java/org/example/generator/Generator.java index 9d86bfb..3529a47 100644 --- a/src/main/java/org/example/generator/Generator.java +++ b/src/main/java/org/example/generator/Generator.java @@ -1,18 +1,326 @@ package org.example.generator; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.Random; +import java.lang.reflect.*; +import java.util.*; +import java.util.concurrent.ThreadLocalRandom; +@SuppressWarnings("unchecked") public class Generator { + private final Random rnd; - public Object generateValueOfType(Class clazz) throws InvocationTargetException, InstantiationException, IllegalAccessException { - Constructor[] constructors = clazz.getDeclaredConstructors(); + private static final int P_NULL = 10; + private static final int P_REUSE = 50; - int randomConstructorIndex = new Random().nextInt(constructors.length); - Constructor randomConstructor = constructors[randomConstructorIndex]; - return randomConstructor.newInstance(111); + public Generator() { + this(ThreadLocalRandom.current()); } + public Generator(int ignoredDepth) { + this(ThreadLocalRandom.current()); + } + + public Generator(Random rnd) { + this.rnd = Objects.requireNonNull(rnd); + } + + public T generateValueOfType(Class clazz) throws ReflectiveOperationException { + State st = new State(rnd); + return (T) generateGraphForClass(clazz, st); + } + + private static final class State { + final Random rnd; + final ArrayList pool = new ArrayList<>(); + State(Random rnd) { this.rnd = rnd; } + } + + private Object generateGraphForClass(Class type, State st) throws ReflectiveOperationException { + if (type.isPrimitive() || isWrapperOrString(type)) return primitiveValue(type); + if (type.isEnum()) return pickEnumValue((Class>) type); + if (type.isArray()) return randomArray(type.getComponentType(), st); + if (Optional.class == type) return Optional.empty(); // сырая Optional без параметра + + Class concrete = resolveConcrete(type); + + if (Collection.class.isAssignableFrom(concrete)) { + return instantiateCollectionClass(concrete); + } + if (Map.class.isAssignableFrom(concrete)) { + return instantiateMapClass(concrete); + } + + Object shell = instantiateShell(concrete); + st.pool.add(shell); + + populateFields(shell, st); + return shell; + } + + private boolean isWrapperOrString(Class c) { + return c == Boolean.class || c == Byte.class || c == Short.class || c == Integer.class + || c == Long.class || c == Float.class || c == Double.class + || c == Character.class || c == String.class; + } + + private Object primitiveValue(Class c) { + if (c == boolean.class || c == Boolean.class) return rnd.nextBoolean(); + if (c == byte.class || c == Byte.class) return (byte) rnd.nextInt(); + if (c == short.class || c == Short.class) return (short) rnd.nextInt(); + if (c == int.class || c == Integer.class) return rnd.nextInt(10_000); + if (c == long.class || c == Long.class) return rnd.nextLong(); + if (c == float.class || c == Float.class) return rnd.nextFloat() * 1000f; + if (c == double.class || c == Double.class) return rnd.nextDouble() * 1000d; + if (c == char.class || c == Character.class) return (char) ('a' + rnd.nextInt(26)); + if (c == String.class) return randomString(3 + rnd.nextInt(8)); + throw new IllegalArgumentException("Unsupported primitive: " + c); + } + + private String randomString(int n) { + var sb = new StringBuilder(n); + for (int i = 0; i < n; i++) { + int t = rnd.nextInt(3); + if (t == 0) sb.append((char) ('a' + rnd.nextInt(26))); + else if (t == 1) sb.append((char) ('A' + rnd.nextInt(26))); + else sb.append((char) ('0' + rnd.nextInt(10))); + } + return sb.toString(); + } + + private Object pickEnumValue(Class> e) { + Object[] vals = e.getEnumConstants(); + if (vals == null || vals.length == 0) return null; + return vals[rnd.nextInt(vals.length)]; + } + + private Object randomArray(Class component, State st) throws ReflectiveOperationException { + int size = rnd.nextInt(3); // 0..2 + Object arr = Array.newInstance(component, size); + for (int i = 0; i < size; i++) { + Object v = generateReferenceLike(component, null, st); + Array.set(arr, i, v); + } + return arr; + } + + private Class resolveConcrete(Class type) { + if (!type.isInterface() && !Modifier.isAbstract(type.getModifiers())) { + return type; + } + Generatable ann = type.getAnnotation(Generatable.class); + if (ann == null || ann.implementations().length == 0) { + throw new IllegalArgumentException("Нельзя сгенерировать " + type.getName()); + } + Class[] impls = ann.implementations(); + return impls[rnd.nextInt(impls.length)]; + } + + private Object instantiateShell(Class type) throws ReflectiveOperationException { + if (!type.isInterface() && !Modifier.isAbstract(type.getModifiers())) { + Generatable ann = type.getAnnotation(Generatable.class); + if (ann == null && !type.getName().startsWith("java.")) { + throw new IllegalArgumentException("Класс " + type.getName() + " не помечен @Generatable"); + } + } + + // Берём конструкторы в случайном порядке + Constructor[] ctors = type.getDeclaredConstructors(); + List> shuffled = new ArrayList<>(Arrays.asList(ctors)); + Collections.shuffle(shuffled, rnd); + + for (Constructor ctor : shuffled) { + try { + ctor.setAccessible(true); + Class[] ptypes = ctor.getParameterTypes(); + Object[] args = new Object[ptypes.length]; + for (int i = 0; i < ptypes.length; i++) { + Class p = ptypes[i]; + if (p.isPrimitive() || isWrapperOrString(p) || p.isEnum()) { + args[i] = primitiveValue(p); + } else if (p.isArray()) { + // оболочка: пустой массив + args[i] = Array.newInstance(p.getComponentType(), 0); + } else if (Optional.class.isAssignableFrom(p)) { + args[i] = Optional.empty(); + } else if (Collection.class.isAssignableFrom(p)) { + args[i] = instantiateCollectionClass(p); + } else if (Map.class.isAssignableFrom(p)) { + args[i] = instantiateMapClass(p); + } else { + args[i] = null; + } + } + return ctor.newInstance(args); + } catch (Throwable ignore) { + // пробуем следующий случайный конструктор + } + } + + Constructor noArg = type.getDeclaredConstructor(); + noArg.setAccessible(true); + return noArg.newInstance(); + } + private void populateFields(Object obj, State st) throws ReflectiveOperationException { + Class c = obj.getClass(); + for (Field f : allFields(c)) { + if (Modifier.isStatic(f.getModifiers())) continue; + if (Modifier.isFinal(f.getModifiers())) continue; + f.setAccessible(true); + + Object cur = f.get(obj); + Class ft = f.getType(); + Type gft = f.getGenericType(); + + if (cur != null) { + if (cur instanceof Collection col) { + maybeFillCollection((Collection) col, gft, st, obj); + } else if (cur instanceof Map map) { + maybeFillMap((Map) map, gft, st, obj); + } + continue; + } + + Object val = generateForField(ft, gft, st, obj); + if (val != null || !ft.isPrimitive()) { + f.set(obj, val); + } else { + f.set(obj, primitiveValue(ft)); + } + } + } + + private List allFields(Class c) { + ArrayList res = new ArrayList<>(); + for (Class k = c; k != null && k != Object.class; k = k.getSuperclass()) { + res.addAll(Arrays.asList(k.getDeclaredFields())); + } + return res; + } + + private Object generateForField(Class ft, Type gft, State st, Object owner) throws ReflectiveOperationException { + if (ft.isPrimitive() || isWrapperOrString(ft)) return primitiveValue(ft); + if (ft.isEnum()) return pickEnumValue((Class>) ft); + + if (ft.isArray()) { + int n = rnd.nextInt(3); + Object arr = Array.newInstance(ft.getComponentType(), n); + for (int i = 0; i < n; i++) { + Array.set(arr, i, generateReferenceLike(ft.getComponentType(), owner, st)); + } + return arr; + } + + if (Optional.class.isAssignableFrom(ft)) { + Type arg = (gft instanceof ParameterizedType pt) ? pt.getActualTypeArguments()[0] : Object.class; + boolean present = rnd.nextBoolean(); + if (!present) return Optional.empty(); + Object elem = generateForTypeToken(arg, st, owner); + return Optional.ofNullable(elem); + } + + if (Collection.class.isAssignableFrom(ft)) { + Collection c = instantiateCollectionClass(ft); + maybeFillCollection(c, gft, st, owner); + return c; + } + + if (Map.class.isAssignableFrom(ft)) { + Map m = instantiateMapClass(ft); + maybeFillMap(m, gft, st, owner); + return m; + } + + return generateReferenceLike(ft, owner, st); + } + + private Object generateForTypeToken(Type t, State st, Object owner) throws ReflectiveOperationException { + if (t instanceof Class c) { + return generateForField(c, c, st, owner); + } else if (t instanceof ParameterizedType pt) { + Class raw = (Class) pt.getRawType(); + return generateForField(raw, pt, st, owner); + } else { + return null; + } + } + + private void maybeFillCollection(Collection c, Type gft, State st, Object owner) throws ReflectiveOperationException { + Type elemType = (gft instanceof ParameterizedType pt) ? pt.getActualTypeArguments()[0] : Object.class; + int add = rnd.nextInt(3); // 0..2 + for (int i = 0; i < add; i++) { + c.add(generateForTypeToken(elemType, st, owner)); + } + } + + private void maybeFillMap(Map m, Type gft, State st, Object owner) throws ReflectiveOperationException { + Type kt = Object.class, vt = Object.class; + if (gft instanceof ParameterizedType pt) { + kt = pt.getActualTypeArguments()[0]; + vt = pt.getActualTypeArguments()[1]; + } + int add = rnd.nextInt(3); + for (int i = 0; i < add; i++) { + Object k = generateForTypeToken(kt, st, owner); + Object v = generateForTypeToken(vt, st, owner); + m.put(k, v); + } + } + + private Object generateReferenceLike(Class ft, Object owner, State st) throws ReflectiveOperationException { + if (!ft.isPrimitive() && rnd.nextInt(100) < P_NULL) return null; + + ArrayList candidates = new ArrayList<>(); + for (Object o : st.pool) { + if (o != owner && ft.isInstance(o)) candidates.add(o); + } + if (!candidates.isEmpty() && rnd.nextInt(100) < P_REUSE) { + return candidates.get(rnd.nextInt(candidates.size())); + } + + Class concrete = resolveConcrete(ft); + if (concrete.isPrimitive() || isWrapperOrString(concrete) || concrete.isEnum() || concrete.isArray()) { + return generateGraphForClass(concrete, st); + } + if (Collection.class.isAssignableFrom(concrete)) { + return instantiateCollectionClass(concrete); + } + if (Map.class.isAssignableFrom(concrete)) { + return instantiateMapClass(concrete); + } + + Object shell = instantiateShell(concrete); + st.pool.add(shell); + populateFields(shell, st); + return shell; + } + + + private Collection instantiateCollectionClass(Class raw) { + if (raw == List.class || raw == Collection.class) return new ArrayList<>(); + if (raw == Set.class) return new HashSet<>(); + if (Collection.class.isAssignableFrom(raw)) { + try { + Constructor c = raw.getDeclaredConstructor(); + c.setAccessible(true); + return (Collection) c.newInstance(); + } catch (Exception e) { + return new ArrayList<>(); + } + } + return new ArrayList<>(); + } + + private Map instantiateMapClass(Class raw) { + if (raw == Map.class) return new LinkedHashMap<>(); + if (Map.class.isAssignableFrom(raw)) { + try { + Constructor c = raw.getDeclaredConstructor(); + c.setAccessible(true); + return (Map) c.newInstance(); + } catch (Exception e) { + return new LinkedHashMap<>(); + } + } + return new LinkedHashMap<>(); + } } diff --git a/src/test/java/org/example/generator/GeneratorTest.java b/src/test/java/org/example/generator/GeneratorTest.java new file mode 100644 index 0000000..bc28dea --- /dev/null +++ b/src/test/java/org/example/generator/GeneratorTest.java @@ -0,0 +1,110 @@ +package org.example.generator; + +import org.example.classes.BinaryTreeNode; +import org.example.classes.Cart; +import org.example.classes.Example; +import org.example.classes.Product; +import org.example.classes.Rectangle; +import org.example.classes.Shape; +import org.example.classes.Triangle; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; + +import java.time.Duration; +import java.util.EnumSet; +import java.util.IdentityHashMap; +import java.util.LinkedList; +import java.util.Queue; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.junit.jupiter.api.Assertions.assertTrue; + +public class GeneratorTest { + + @Test + void generateCartWithProducts() throws Exception { + Generator gen = new Generator(3); + Cart cart = gen.generateValueOfType(Cart.class); + assertNotNull(cart); + assertNotNull(cart.getItems(), "Cart.getItems() не должен быть null"); + } + + enum LocalE { A, B, C } + + @Test + void generateEnum() throws Exception { + Generator gen = new Generator(3); + LocalE e = gen.generateValueOfType(LocalE.class); + assertNotNull(e); + assertTrue(EnumSet.allOf(LocalE.class).contains(e)); + } + + @Test + void unannotatedInterfaceFails() { + interface Unannotated {} + Generator gen = new Generator(3); + IllegalArgumentException ex = assertThrows(IllegalArgumentException.class, + () -> gen.generateValueOfType(Unannotated.class)); + assertTrue(ex.getMessage().contains("Нельзя сгенерировать")); + } + + static Stream> allTypes() { + return Stream.of( + Example.class, + Product.class, + Cart.class, + Rectangle.class, + Triangle.class, + BinaryTreeNode.class, + Shape.class + ); + } + + @ParameterizedTest + @MethodSource("allTypes") + void generatesAllKnownTypes(Class type) { + Generator gen = new Generator(3); + + Object obj = assertTimeoutPreemptively( + Duration.ofSeconds(1), + () -> gen.generateValueOfType((Class) type) + ); + + assertNotNull(obj); + + if (type == Shape.class) { + assertTrue(obj instanceof Rectangle || obj instanceof Triangle); + } + } + @Test + void generatesGraphWithoutHanging_BinaryTreeNode() { + Generator gen = new Generator(new java.util.Random(42)); + BinaryTreeNode root = assertTimeoutPreemptively( + Duration.ofSeconds(1), + () -> gen.generateValueOfType(BinaryTreeNode.class) + ); + assertNotNull(root); + + IdentityHashMap seen = new IdentityHashMap<>(); + Queue q = new LinkedList<>(); + q.add(root); + while (!q.isEmpty() && seen.size() < 10_000) { + BinaryTreeNode n = q.poll(); + if (n == null || seen.put(n, Boolean.TRUE) != null) continue; + q.add(n.getLeft()); + q.add(n.getRight()); + } + assertTrue(seen.size() >= 1); + } + + @Test + void generatesGraphWithSharingForInterface() throws Exception { + Generator gen = new Generator(new java.util.Random(123)); + Object s = gen.generateValueOfType(Shape.class); + assertNotNull(s); + } +}