Nikita Salnikov Tarnovski is a senior developer at Plumbr and an expert in application performance tuning, with years of experience in performance optimization. Recently, Tarnovski wrote an article discussing six Java features that ordinary developers should avoid as much as possible. These features are commonly found in various frameworks or libraries, but for regular application developers, using them might bring disaster to the applications they are developing.
I have spent countless hours debugging various applications. Based on past experience, I can conclude that most developers should stay away from certain Java SE features or APIs. Here, "most developers" refers to regular Java EE developers rather than library designers or infrastructure developers.
Frankly speaking, most teams should avoid the following Java features in the long run. However, there are always exceptions. If you have a strong team that is always aware of what it is doing, then go ahead and do as you please. But for most cases, if you use the following Java features in your project development, you will likely regret it in the long term.
The Java features to avoid are:
Reflection
Bytecode manipulation
ThreadLocal
Class loaders
Weak references and soft references
Sockets
Below, we analyze these features one by one to understand why regular Java developers should avoid them:
Reflection: Reflection has its place in popular libraries like Spring and Hibernate. However, introspecting business code is often not a good idea for many reasons, and in general, I always recommend avoiding reflection.
First, consider code readability and tool support. In a familiar IDE, it's easy to find internal dependencies in your Java code. Now try replacing your code with reflection and see how it goes. If you modify the state of encapsulated objects through reflection, the results will become even more unpredictable. Consider the following example:
If you do this, you lose compile-time safety guarantees. As shown in the example above, if the parameter passed to the getDeclaredField() method is incorrect, the error will only be discovered at runtime. Keep in mind that finding runtime bugs is much harder than finding compile-time bugs.
Finally, let's talk about cost. JIT optimizes reflection differently, with some optimizations taking longer and others being impossible to apply. Therefore, the performance loss caused by reflection can sometimes differ by several orders of magnitude. However, in typical business applications, you may not notice this cost.
In summary, I believe the only reasonable (direct) use of reflection in business code is through AOP. Beyond that, it’s best to steer clear of reflection.
Bytecode Manipulation: If CGLIB or ASM libraries are used directly in Java EE application code, I suggest you take a closer look. Just as I mentioned earlier about the negative effects of reflection, the pain caused by bytecode manipulation could be several times worse.
Worse still, during compilation, you cannot see the executable code. Essentially, you don't know what code is actually running in production. Therefore, when facing runtime issues and debugging, you will spend more time.
ThreadLocal: Seeing ThreadLocal in business code makes me shudder for two reasons. First, with ThreadLocal, you can pass variables without explicitly passing them through method calls, and you may become addicted to this practice. In some cases, this might be reasonable, but if you're not careful, I guarantee that your code will end up with a lot of unexpected dependencies.
The second reason relates to my daily work. Storing data in ThreadLocal can easily cause memory leaks. At least one out of ten permanent generation leaks I've seen was caused by excessive use of ThreadLocal. Combined with class loaders and thread pools, "java.lang.OutOfMemoryError: Permgen space" is just around the corner.
Class Loaders: First, class loaders are complex things. You must first understand them, including their hierarchy, delegation mechanisms, and class caching, among other things. Even if you think you've mastered class loaders, various problems will arise when you start using them, which may lead to class loader leakage issues. Therefore, I recommend leaving class loaders to the application server.
Weak References and Soft References: Do you only know what weak references and soft references are and how to use them simply? Now that you have a better understanding of the Java core, would you rewrite all caches using soft references? That wouldn't be a good idea—just because you have a hammer doesn’t mean everything is a nail.
You might be wondering why I say that caches are not suitable for using soft references. After all, building caches with soft references is a great way to delegate some complexity to the GC instead of implementing it yourself.
Let's take an example. You built a cache using soft references, so when memory is about to run out, the GC will intervene and start cleaning up. But now you can't control which objects will be removed from the cache, and it's very likely that the object will need to be recreated the next time it's no longer in the cache. If memory remains tight and triggers another GC cleanup, a potential infinite loop may occur where the application consumes a lot of CPU time, and Full GCs keep happening.
Sockets: java.net.Socket is incredibly difficult to use. I believe its flaws stem from its blocking nature. When writing typical Java EE applications with Web frontends, you need high concurrency to support a large number of user accesses. The last thing you want is for your less scalable thread pool to sit there waiting for blocking Sockets.
Nowadays, excellent third-party libraries have emerged to solve these problems. Don't write your own code; give Netty a try.
InfoQ readers, since its inception, Java has undergone multiple version updates, each bringing many new features. In daily Java development, what Java features do you think are prone to causing problems? The author mentions that reflection should not be used in regular application development, but for the development of some frameworks or libraries, it is practically impossible to implement without reflection, such as in Spring and Struts2. So, in general Java project development, where do you think reflection is necessary? In other words, where would functionality or requirements be unachievable without reflection? The author also does not recommend using bytecode manipulation, but in reality, some frameworks must use it to implement certain features, such as Spring using both Java dynamic proxies and the CGLib library to achieve AOP. So, for general Java projects, where would bytecode manipulation be required? We welcome all readers to share their thoughts and discuss these interesting topics together.