1 /*
2 * Copyright (c) 2019, 2026, Oracle and/or its affiliates. All rights reserved.
3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
4 *
5 * This code is free software; you can redistribute it and/or modify it
6 * under the terms of the GNU General Public License version 2 only, as
7 * published by the Free Software Foundation.
8 *
9 * This code is distributed in the hope that it will be useful, but WITHOUT
10 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
11 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
12 * version 2 for more details (a copy is included in the LICENSE file that
13 * accompanied this code).
14 *
15 * You should have received a copy of the GNU General Public License version
16 * 2 along with this work; if not, write to the Free Software Foundation,
17 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
18 *
19 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
20 * or visit www.oracle.com if you need additional information or have any
21 * questions.
22 */
23
24 /*
25 * @test
26 * @summary Test serialization of value classes
27 * @enablePreview
28 * @modules java.base/jdk.internal.value
29 * @library /test/lib
30 * @compile ValueSerializationTest.java
31 * @build jdk.test.lib.helpers.StrictInit jdk.test.lib.helpers.StrictProcessor
32 * @comment run the StrictProcessor over the classes that use \@StrictInit to
33 * generate classfiles with STRICT_INIT access flags for its annotated fields
34 * @run driver jdk.test.lib.helpers.StrictProcessor
35 * ValueSerializationTest$IdentityStrictPoint
36 * @run junit ${test.main.class}
37 */
38
39 import java.io.ByteArrayInputStream;
40 import java.io.ByteArrayOutputStream;
41 import java.io.DataOutputStream;
42 import java.io.Externalizable;
43 import java.io.IOException;
44 import java.io.InvalidClassException;
45 import java.io.InvalidObjectException;
46 import java.io.NotSerializableException;
47 import java.io.ObjectInput;
48 import java.io.ObjectInputStream;
49 import java.io.ObjectOutput;
50 import java.io.ObjectOutputStream;
51 import java.io.ObjectStreamClass;
52 import java.io.ObjectStreamException;
53 import java.io.Serial;
54 import java.io.Serializable;
55 import java.util.stream.Stream;
56
57 import jdk.internal.value.Deserializer;
58 import jdk.test.lib.helpers.StrictInit;
59 import org.junit.jupiter.params.ParameterizedTest;
60 import org.junit.jupiter.params.provider.Arguments;
61 import org.junit.jupiter.params.provider.MethodSource;
62 import static java.io.ObjectStreamConstants.SC_EXTERNALIZABLE;
63 import static java.io.ObjectStreamConstants.SC_SERIALIZABLE;
64 import static java.io.ObjectStreamConstants.STREAM_MAGIC;
65 import static java.io.ObjectStreamConstants.STREAM_VERSION;
66 import static java.io.ObjectStreamConstants.TC_CLASSDESC;
67 import static java.io.ObjectStreamConstants.TC_ENDBLOCKDATA;
68 import static java.io.ObjectStreamConstants.TC_NULL;
69 import static java.io.ObjectStreamConstants.TC_OBJECT;
70 import static org.junit.jupiter.api.Assertions.assertArrayEquals;
71 import static org.junit.jupiter.api.Assertions.assertEquals;
72 import static org.junit.jupiter.api.Assertions.assertThrows;
73 import static org.junit.jupiter.api.Assertions.assertTrue;
74
75 public class ValueSerializationTest {
76
77 private static final Class<NotSerializableException> NSE = NotSerializableException.class;
78 private static final Class<InvalidClassException> ICE = InvalidClassException.class;
79
80 static Stream<Arguments> serializationFailingInstances() {
81 return Stream.of(
82 Arguments.of(
83 new NonSerializableValue(10, 100),
84 NSE
85 ),
86
87 Arguments.of(
88 new ValueWithNoDeserializer(10, 100),
89 ICE
90 ),
91
92 Arguments.of(
93 new IdentityStrictPoint(),
94 ICE
95 ),
96
97 Arguments.of(
98 new StrictInSuper(),
99 ICE
100 ),
101
102 Arguments.of(
103 new StrictInTwiceSuper(),
104 ICE
105 ),
106
107 Arguments.of(
108 new StrictInAbstractValueSuper(),
109 ICE
110 ),
111
112 // an array of non-serializable value objects
113 Arguments.of(
114 new NonSerializableValue[]{
115 new NonSerializableValue(1, 5)
116 },
117 NSE
118 ),
119
120 Arguments.of(
121 new Object[]{
122 new NonSerializableValue(3, 7)
123 },
124 NSE
125 ),
126
127 Arguments.of(
128 new ExternalizableValue(12, 102),
129 ICE
130 ),
131
132 Arguments.of(
133 new ExternalizableValue[]{
134 new ExternalizableValue(3, 7),
135 new ExternalizableValue(2, 8)
136 },
137 ICE
138 ),
139
140 Arguments.of(
141 new Object[]{
142 new ExternalizableValue(13, 17),
143 new ExternalizableValue(14, 18)
144 },
145 ICE
146 )
147 );
148 }
149
150 /*
151 * Verifies that the given obj that isn't expected to be serializable
152 * throws the expected exception from ObjectOutputStream.writeObject()
153 */
154 @ParameterizedTest
155 @MethodSource("serializationFailingInstances")
156 void testSerializationFails(Object obj, Class<? extends Exception> expectedException)
157 throws Exception {
158 // expect serialization to fail
159 try (ObjectOutputStream oos = new ObjectOutputStream(new ByteArrayOutputStream())) {
160 assertThrows(expectedException, () -> oos.writeObject(obj));
161 }
162 }
163
164 static Stream<Object> serializingInstances() {
165 return Stream.of(
166 new ValueWithDeserializer(11, 101),
167
168 new ValueWithDeserializer[]{
169 new ValueWithDeserializer(1, 5),
170 new ValueWithDeserializer(2, 6)
171 },
172
173 new Object[]{
174 new ValueWithDeserializer(3, 7),
175 new ValueWithDeserializer(4, 8)
176 },
177
178 new ValueWriteReplaceWithIdentity(45),
179
180 new ValueWriteReplaceWithIdentity[]{
181 new ValueWriteReplaceWithIdentity(46)
182 },
183
184 new ExtValueWithIdentityReplacement("hello"),
185
186 new ExtValueWithIdentityReplacement[]{
187 new ExtValueWithIdentityReplacement("there")
188 },
189
190 new CustomNumberWithIdentity(16, 42),
191
192 new CustomNumberWithIdentity[] {
193 new CustomNumberWithIdentity(-6, 77),
194 new CustomNumberWithIdentity(54, -79),
195 }
196 );
197 }
198
199 /*
200 * Verifies that a value object that implements java.io.Serializable and is associated with
201 * the JDK internal jdk.internal.value.Deserializer can be serialized and deserialized through
202 * the use of ObjectOutputStream.writeObject() and ObjectInputStream.readObject() successfully.
203 * The deserialized object is then compared with the given obj to verify that they are equal.
204 */
205 @ParameterizedTest
206 @MethodSource("serializingInstances")
207 void testSerDeserSucceeds(Object obj) throws IOException, ClassNotFoundException {
208 // serialize
209 ByteArrayOutputStream baos = new ByteArrayOutputStream();
210 try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
211 oos.writeObject(obj);
212 }
213 byte[] bytes = baos.toByteArray();
214 Object actual;
215 // deserialize
216 try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes))) {
217 actual = ois.readObject();
218 }
219 // compare the deserialized with the original
220 if (obj.getClass().isArray()) {
221 assertArrayEquals((Object[]) obj, (Object[]) actual);
222 } else {
223 assertEquals(obj, actual);
224 }
225 }
226
227 static Stream<Arguments> classes() {
228 return Stream.of(
229 Arguments.of(
230 ExtValueWithIdentityReplacement.class,
231 SC_EXTERNALIZABLE,
232 ICE
233 ),
234
235 Arguments.of(
236 ExtValueWithIdentityReplacement.class,
237 SC_SERIALIZABLE,
238 ICE
239 ),
240
241 Arguments.of(
242 IdentityStrictPoint.class,
243 SC_SERIALIZABLE,
244 ICE
245 ),
246
247 Arguments.of(
248 StrictInSuper.class,
249 SC_SERIALIZABLE,
250 ICE
251 ),
252
253 Arguments.of(
254 StrictInTwiceSuper.class,
255 SC_SERIALIZABLE,
256 ICE
257 ),
258
259 Arguments.of(
260 StrictInAbstractValueSuper.class,
261 SC_SERIALIZABLE,
262 ICE
263 ),
264
265 Arguments.of(
266 ValueWithDeserializer.class,
267 SC_EXTERNALIZABLE,
268 ICE
269 ),
270
271 Arguments.of(
272 ValueWithDeserializer.class,
273 SC_SERIALIZABLE,
274 null
275 ),
276
277 Arguments.of(
278 CustomNumberWithIdentity.class,
279 SC_SERIALIZABLE,
280 null
281 )
282 );
283 }
284
285 /*
286 * A byte stream is generated containing a reference to the given class
287 * with the given flags and a serial version UID determined in the test method.
288 * The byte stream is then read using ObjectInputStream.readObject() and the test verifies
289 * that if an exception is expected to be thrown then it is thrown, or if the deserialization
290 * is expected to complete normally, then it verifies that no exception is thrown.
291 */
292 @ParameterizedTest
293 @MethodSource("classes")
294 void testDeser(Class<?> clazz, byte flags, Class<? extends Exception> expectedException)
295 throws Exception {
296 ObjectStreamClass clsDesc = ObjectStreamClass.lookup(clazz);
297 long uid = clsDesc == null ? 0L : clsDesc.getSerialVersionUID();
298 byte[] serialBytes = byteStreamFor(clazz.getName(), uid, flags);
299 // deserialize
300 try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(serialBytes))) {
301 if (expectedException != null) {
302 assertThrows(expectedException, () -> ois.readObject());
303 } else {
304 ois.readObject();
305 }
306 }
307 }
308
309 // Generate a byte stream containing a reference to the named class with the SVID and flags.
310 private static byte[] byteStreamFor(String className, long uid, byte flags) throws Exception {
311 ByteArrayOutputStream baos = new ByteArrayOutputStream();
312 DataOutputStream dos = new DataOutputStream(baos);
313 dos.writeShort(STREAM_MAGIC);
314 dos.writeShort(STREAM_VERSION);
315 dos.writeByte(TC_OBJECT);
316 dos.writeByte(TC_CLASSDESC);
317 dos.writeUTF(className);
318 dos.writeLong(uid);
319 dos.writeByte(flags);
320 dos.writeShort(0); // number of fields
321 dos.writeByte(TC_ENDBLOCKDATA); // no annotations
322 dos.writeByte(TC_NULL); // no superclasses
323 dos.close();
324 return baos.toByteArray();
325 }
326
327 /**
328 * A concrete value class that doesn't implement Serializable (or Externalizable) interface
329 */
330 public static value class NonSerializableValue {
331 public int x;
332 public int y;
333
334 public NonSerializableValue(int x, int y) {
335 this.x = x;
336 this.y = y;
337 }
338
339 @Override
340 public String toString() {
341 return "[NonSerializableValue x=" + x + " y=" + y + "]";
342 }
343 }
344
345 /**
346 * An identity class with strict initialized instance fields
347 */
348 public static class IdentityStrictPoint implements Serializable {
349 static {
350 for (var f : IdentityStrictPoint.class.getDeclaredFields()) {
351 assertTrue(f.isStrictInit(), "missing strict init on field: " + f.getName());
352 }
353 }
354
355 @StrictInit
356 public int x;
357 @StrictInit
358 public int y;
359
360 public IdentityStrictPoint() {
361 x = 3;
362 y = 5;
363 super();
364 }
365
366 @Override
367 public String toString() {
368 return "[IdentityStrictPoint x=" + x + " y=" + y + "]";
369 }
370 }
371
372 /**
373 * A concrete value class that implements java.io.Serializable and doesn't have any
374 * jdk.internal.value.Deserializer on its constructor.
375 */
376 public static value class ValueWithNoDeserializer implements Serializable {
377 public int x;
378 public int y;
379
380 // Note: Must NOT have @Deserializer annotation
381 public ValueWithNoDeserializer(int x, int y) {
382 this.x = x;
383 this.y = y;
384 }
385
386 @Override
387 public String toString() {
388 return "[ValueWithNoDeserializer x=" + x + " y=" + y + "]";
389 }
390 }
391
392 /**
393 * A concrete value class which implements java.io.Externalizable and doesn't
394 * implement writeReplace().
395 */
396 static value class ExternalizableValue implements Externalizable {
397 public int x;
398 public int y;
399
400 public ExternalizableValue() {
401 this.x = 0;
402 this.y = 0;
403 }
404
405 ExternalizableValue(int x, int y) {
406 this.x = x;
407 this.y = y;
408 }
409
410 @Override
411 public void readExternal(ObjectInput in) {
412 // concrete value class isn't expected to be deserializable, so we don't
413 // expect this method to be invoked during deserialization.
414 throw new AssertionError("not expected to be invoked on " + this);
415 }
416
417 @Override
418 public void writeExternal(ObjectOutput out) {
419 // concrete value class isn't expected to be serializable, so we don't
420 // expect this method to be invoked during serialization.
421 throw new AssertionError("not expected to be invoked on " + this);
422 }
423
424 @Override
425 public String toString() {
426 return "[ExternalizableValue x=" + x + " y=" + y + "]";
427 }
428 }
429
430
431 /**
432 * A concrete value class which implements java.io.Serializable and has a
433 * jdk.internal.value.Deserializer associated with its constructor.
434 */
435 static value class ValueWithDeserializer implements Serializable {
436 public int x;
437 public int y;
438
439 @Deserializer({"x", "y"})
440 private ValueWithDeserializer(int x, int y) {
441 this.x = x;
442 this.y = y;
443 }
444
445 @Override
446 public String toString() {
447 return "[ValueWithDeserializer x=" + x + " y=" + y + "]";
448 }
449 }
450
451 /**
452 * A concrete value class which implements java.io.Serializable
453 * and implements the writeReplace() method to return an identity
454 * object.
455 */
456 static value class ValueWriteReplaceWithIdentity implements Serializable {
457 public int x;
458
459 ValueWriteReplaceWithIdentity(int x) {
460 this.x = x;
461 }
462
463 @Serial
464 Object writeReplace() throws ObjectStreamException {
465 return new IdentityRecord(x);
466 }
467
468 @Serial
469 private void readObject(ObjectInputStream s) throws InvalidObjectException {
470 // the writeReplace() implementation of this class, when the serialization side
471 // is preparing to write this object to the stream, has replaced this object
472 // with an instance of a different class, so we don't expect deserialization
473 // to invoke this method.
474 throw new AssertionError("not expected to be invoked on " + this);
475 }
476
477 @Override
478 public String toString() {
479 return "[ValueWriteReplaceWithIdentity x=" + x + "]";
480 }
481
482 private record IdentityRecord(int x) implements Serializable {
483 @Serial
484 Object readResolve() throws ObjectStreamException {
485 return new ValueWriteReplaceWithIdentity(x);
486 }
487 }
488 }
489
490 /**
491 * A concrete value class which implements java.io.Externalizable and implements
492 * the writeReplace() method to return an identity object.
493 */
494 static value class ExtValueWithIdentityReplacement implements Externalizable {
495 public String s;
496
497 ExtValueWithIdentityReplacement(String s) {
498 this.s = s;
499 }
500
501 @Override
502 public boolean equals(Object other) {
503 return other instanceof ExtValueWithIdentityReplacement foo && s.equals(foo.s);
504 }
505
506 @Serial
507 Object writeReplace() throws ObjectStreamException {
508 return new IdentityRecord(s);
509 }
510
511 private record IdentityRecord(String s) implements Serializable {
512 @Serial
513 Object readResolve() throws ObjectStreamException {
514 return new ExtValueWithIdentityReplacement(s);
515 }
516 }
517
518 @Override
519 public void readExternal(ObjectInput in) {
520 // the writeReplace() implementation of this class, when the serialization side
521 // is preparing to write this object to the stream, has replaced this object
522 // with an instance of a different class, so we don't expect deserialization
523 // to invoke this method.
524 throw new AssertionError("not expected to be invoked on " + this);
525 }
526
527 @Override
528 public void writeExternal(ObjectOutput out) {
529 // the writeReplace() implementation of this class, when the serialization side
530 // is preparing to write this object to the stream, has replaced this object
531 // with an instance of a different class, so we don't expect this method to
532 // play any role during serialization.
533 throw new AssertionError("not expected to be invoked on " + this);
534 }
535
536 @Override
537 public String toString() {
538 return "[ExtValueWithIdentityReplacement s=" + s + "]";
539 }
540 }
541
542 /**
543 * A plain identity class that does not declare any strictly initialized
544 * instance field but its immediate superclasses, which implement Serializable,
545 * does.
546 */
547 public static class StrictInSuper extends IdentityStrictPoint {
548 // Declares no field, inherits strictly-initialized instance fields
549 // IdentityStrictPoint.x and y
550 public StrictInSuper() {
551 super();
552 }
553
554 @Override
555 public String toString() {
556 return "[StrictInSuper x=" + x + " y=" + y + "]";
557 }
558 }
559
560 /**
561 * A plain identity class that does not declare any strictly initialized
562 * instance field but one of its non-immediate superclasses that implement
563 * Serializable does.
564 */
565 public static class StrictInTwiceSuper extends StrictInSuper {
566 // Declares no field, inherits strictly-initialized instance fields
567 // IdentityStrictPoint.x and y
568 public StrictInTwiceSuper() {
569 super();
570 }
571
572 @Override
573 public String toString() {
574 return "[StrictInTwiceSuper x=" + x + " y=" + y + "]";
575 }
576 }
577
578 /**
579 * An abstract value class that declares instance fields. Such fields are
580 * always strictly initialized, making none of their subclasses serializable
581 * without jdk.internal.value.Deserializer.
582 */
583 public abstract static value class HasFieldAbstractValue implements Serializable {
584 public int x;
585 public int y;
586
587 public HasFieldAbstractValue(int x, int y) {
588 this.x = x;
589 this.y = y;
590 }
591
592 @Override
593 public String toString() {
594 return "[HasFieldAbstractValue x=" + x + " y=" + y + "]";
595 }
596 }
597
598 /**
599 * An identity class that inherits strictly-initialized instance fields from
600 * its abstract value superclass that is serializable. Therefore, this class
601 * is not serializable without jdk.internal.value.Deserializer.
602 */
603 public static class StrictInAbstractValueSuper extends HasFieldAbstractValue {
604 public StrictInAbstractValueSuper() {
605 super(42, -3);
606 }
607
608 @Override
609 public String toString() {
610 return "[StrictInAbstractValueSuper x=" + x + " y=" + y + "]";
611 }
612 }
613
614 /**
615 * An identity class that does not inherit any strictly-initialized instance
616 * field from its abstract value superclass that is serializable, in this
617 * case the migrated java.lang.Number. This class is accepted by default
618 * serialization.
619 */
620 public static class CustomNumberWithIdentity extends Number {
621 public int x;
622 public int y;
623
624 public CustomNumberWithIdentity(int x, int y) {
625 this.x = x;
626 this.y = y;
627 }
628
629 @Override
630 public int intValue() {
631 return (int) longValue();
632 }
633
634 @Override
635 public long longValue() {
636 return (long) x << 32 | y;
637 }
638
639 @Override
640 public float floatValue() {
641 return longValue();
642 }
643
644 @Override
645 public double doubleValue() {
646 return longValue();
647 }
648
649 @Override
650 public final boolean equals(Object o) {
651 if (!(o instanceof CustomNumberWithIdentity that))
652 return false;
653
654 return x == that.x && y == that.y;
655 }
656
657 @Override
658 public int hashCode() {
659 int result = x;
660 result = 31 * result + y;
661 return result;
662 }
663
664 @Override
665 public String toString() {
666 return "[CustomNumberWithIdentity x=" + x + " y=" + y + "]";
667 }
668 }
669 }