Why Property‑Copy Tools Fail: Performance Pitfalls and Hidden Type Mismatches in Java
The article analyzes common Java property‑copy utilities, showing how tools like Spring BeanUtils, CGLIB BeanCopier and MapStruct can suffer from poor performance and runtime type‑conversion errors, and recommends using explicit converters generated by IDE plugins to avoid hidden bugs.
Background
Using generic property‑copy utilities (e.g., Spring BeanUtils, CGLIB BeanCopier) can hide type mismatches because Java generics are erased at runtime. The compiler allows copying between objects with incompatible generic fields, leading to ClassCastException or other runtime errors.
Problems with Generic Property‑Copy Tools
Performance may be sub‑optimal.
Some implementations contain bugs.
Type‑safety is not guaranteed; mismatched generic types cause runtime failures.
Example with Spring BeanUtils
import lombok.Data;
import java.util.List;
@Data
public class A {
private String name;
private List<Integer> ids;
}
@Data
public class B {
private String name;
private List<String> ids;
} import org.springframework.beans.BeanUtils;
import java.util.Arrays;
public class BeanUtilDemo {
public static void main(String[] args) {
A first = new A();
first.setName("demo");
first.setIds(Arrays.asList(1, 2, 3));
B second = new B();
BeanUtils.copyProperties(first, second);
// Runtime ClassCastException when iterating over second.getIds()
for (String each : second.getIds()) {
System.out.println(each);
}
}
}The copy copies the raw List reference; the generic type List<String> in B actually holds Integer elements, causing a ClassCastException.
Example with CGLIB BeanCopier
import org.easymock.cglib.beans.BeanCopier;
import java.util.Arrays;
public class BeanUtilDemo {
public static void main(String[] args) {
A first = new A();
first.setName("demo");
first.setIds(Arrays.asList(1, 2, 3));
B second = new B();
BeanCopier copier = BeanCopier.create(A.class, B.class, false);
copier.copy(first, second, null);
// Same runtime exception as above
for (String each : second.getIds()) {
System.out.println(each);
}
}
}Using MapStruct
import org.mapstruct.Mapper;
import org.mapstruct.factory.Mappers;
@Mapper
public interface Converter {
Converter INSTANCE = Mappers.getMapper(Converter.class);
B aToB(A source);
} import java.util.Arrays;
public class BeanUtilDemo {
public static void main(String[] args) {
A first = new A();
first.setName("demo");
first.setIds(Arrays.asList(1, 2, 3));
B second = Converter.INSTANCE.aToB(first);
for (String each : second.getIds()) {
System.out.println(each);
}
}
}MapStruct generates a mapper that safely converts List<Integer> to List<String> at compile time.
import java.util.ArrayList;
import java.util.List;
import javax.annotation.Generated;
import org.springframework.stereotype.Component;
@Generated(value = "org.mapstruct.ap.MappingProcessor",
comments = "version: 1.3.1.Final, compiler: javac, environment: Java 1.8.0_202")
@Component
public class ConverterImpl implements Converter {
@Override
public B aToB(A source) {
if (source == null) {
return null;
}
B target = new B();
target.setName(source.getName());
target.setIds(integerListToStringList(source.getIds()));
return target;
}
protected List<String> integerListToStringList(List<Integer> list) {
if (list == null) {
return null;
}
List<String> result = new ArrayList<>(list.size());
for (Integer i : list) {
result.add(String.valueOf(i));
}
return result;
}
}Although convenient, automatic conversion can mask unintended type changes.
Custom Converter Example
public final class A2BConverter {
public static B from(A source) {
B target = new B();
target.setName(source.getName());
target.setIds(source.getIds()); // compile‑time type check
return target;
}
}Explicit conversion forces the compiler to verify compatible property types, preventing runtime mismatches.
Conclusion
Java generics are erased after compilation, so List<Integer> and List<String> are both represented as raw List at runtime. Property‑copy tools that rely on reflection cannot detect generic mismatches, leading to runtime ClassCastException. MapStruct’s annotation processor reads generic information at compile time and generates type‑safe mappers, but developers must still ensure target types are correct because automatic conversion may hide logic errors. The safest approach is to define explicit converter classes (or let IDE plugins generate them) so that type incompatibilities are caught during compilation and performance overhead is minimized.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
Java Architect Essentials
Committed to sharing quality articles and tutorials to help Java programmers progress from junior to mid-level to senior architect. We curate high-quality learning resources, interview questions, videos, and projects from across the internet to help you systematically improve your Java architecture skills. Follow and reply '1024' to get Java programming resources. Learn together, grow together.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
