Effective Java learning notes - avoid creating unnecessary objects

Posted by eits on Thu, 20 Jan 2022 07:36:52 +0100

Tip: after the article is written, the directory can be generated automatically. Please refer to the help document on the right for how to generate it

preface

Recently, I was reading the Book Effective Java, and I also recorded some contents in the book. Some that I can't understand will be put away for the time being.

Avoid creating unnecessary objects

1. Introduction

Generally speaking, it's better to use a single object in the program instead of creating one as needed. In fact, this method is also very common:

  • springboot @ Autowired annotation
  • Singleton mode
  • Pooling Technology (this category is actually intended to express similar ideas, which are created in advance and used directly)

2. Some suggestions for solution

1. About creating constant String

String s = new String("hello world");

The wrong usage is that it is a String directly derived from new. At this time, the String is allocated in the heap space, and each time new is a different object for the same String. If it is placed in the while loop, thousands of unnecessary instances will be created. The improved method is as follows:

String s = "hello world"

The constants created in this way will be found in the constant pool first. If they cannot be found, they will be put into the constant pool in the heap memory. The next assignment will be directly referenced in the constant pool. The advantages are as follows:

  • It reduces the creation of a large number of unnecessary objects
  • Reduce memory loss and reduce the risk of memory leakage



2. For static factory methods and constructors

For immutable classes that provide static factory methods and constructors, static factory methods should be preferred over constructors, because using constructors will lead to the possibility of creating a pile of unnecessary objects. For example, Boolean Valuef (string) method and Boolean(String) constructor.

//Two static Boolean objects are returned internally, which are globally unique
public static Boolean valueOf(String s) {
        return parseBoolean(s) ? TRUE : FALSE;
}

public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);

It can be seen that the valueOf method actually returns a globally unique Boolean object. If you want to make new by yourself, unnecessary objects will be generated, resulting in waste.
Some tool class methods often used in writing programs are defined as static. In fact, this is also the truth.



3. "Expensive object"

The book gives an example of matching to determine whether a string is a valid Roman numeral

static boolean isRomanNumeral(String s){
	return s.matchs("^(?=.)M*(C[MD] | D?C{0, 3})" + 
		"(X[CL] | L?X{0,3})(I[XV]|V?I{0,3})$");
	)
}

The implementation of this method lies in the string it depends on Matches method. Although this method is easiest to check whether the string matches the regular expression, it is not suitable for performance-oriented situations. Let's check the internal source code method:

public boolean matches(String regex) {
    return Pattern.matches(regex, this);
}

//Pattern.matches
public static boolean matches(String regex, CharSequence input) {
    Pattern p = Pattern.compile(regex);
    Matcher m = p.matcher(input);
    return m.matches();
}

//Pattern.compile
public static Pattern compile(String regex) {
    return new Pattern(regex, 0);
}

After viewing the source code, it is not difficult to find that we passed in a regex regular expression, and the source code is in the underlying Pattern In the compile (regex) method, a Pattern object is created for the expression. If it is in a while loop, thousands of such objects may be created, resulting in a great performance loss. Therefore, we need to modify this writing method. After the transformation, we only create one Pattern object:

Public class RomanNumerals{
	private static final Pattern ROMAN = Pattern.comple(
		"^(?=.)M*(C[MD] | D?C{0, 3})" + 
		"(X[CL] | L?X{0,3})(I[XV]|V?I{0,3})$"
	);
	
	static boolean isRomanNumeral(String s){
		return ROMAN.matcher(s).matches();
	}
}

We directly use the source code to create a globally unique Pattern regular expression object, then match s and skip the original Pattern Compile (regex) is a line of code, so it won't be said that a new Pattern object will be generated once a call is made.

Advantage comparison (results in the book): an 8-character input

  • The original writing took 1.1 μ s
  • The improved writing took 0.17 μ s. 6.5 times faster

Of course, if this static method has not been called, you can actually try lazy loading, but it is not recommended, because it will make the implementation of the method more complex, and the performance of the modified code may not exceed that of the original code, because it will inevitably reduce the time efficiency in the process of lazy loading.



4. Adapter

An adapter refers to an object that delegates functions to a backup object, thereby providing an alternative interface for the backup object. Because the adapter has no other status information except the backup object, it does not need to create multiple adapter instances for a specific adapter of a given object.

For example, the KeySet method in the Map interface returns a collection (Set) of all keys. When we view the source code, we can also see that the keysets returned by the same HashMap always come from one, rather than new each time

public Set<K> keySet() {
    Set<K> ks = this.keySet;
    if (ks == null) {
        ks = new HashMap.KeySet();
        this.keySet = (Set)ks;
    }

    return (Set)ks;
}

Although the returned Set instance can be changed, all returned objects have the same function, so you only need to Set a KeySet. There is no harm in creating multiple KeySet view objects, but there is no need or benefit.



5. Automatic packing and unpacking

If this method is not used properly, it will also create many unnecessary objects. Automatic boxing blurs the difference between the basic type and the boxed basic type, but it does not eliminate it. Sometimes when writing code, you will feel that there is no problem like Integer i = 10 and so on, because there are automatic packing and automatic unpacking, so we don't need to convert, but look at the following code:

//Find the sum of int type data addition
public class TestStatic {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        sum();
        long end = System.currentTimeMillis();
        //Time consumption: 5715 equals approximately 5.715 seconds
        System.out.println("Time consumption:" + (end - start));

    }

    private static long sum(){
        Long sum = 0L;
        for(long i = 0; i <= Integer.MAX_VALUE; i++){
            sum += i;
        }
        return sum;
    }
}

The result of this code is correct, but there is a big problem that the Long type is used instead of Long, which leads to the creation of about 2 31 power Long instances in the 31 power + 1 cycle of 2, and one will be created every time when adding. The running time at this time is about 5.7 seconds, which is a Long time. Let's see how much the time has increased after changing Long to Long:

public class TestStatic {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        sum();
        long end = System.currentTimeMillis();
        //Time consumption: 588 is approximately equal to 0.5 seconds
        System.out.println("Time consumption:" + (end - start));

    }

    private static long sum(){
        long sum = 0L;
        for(long i = 0; i <= Integer.MAX_VALUE; i++){
            sum += i;
        }
        return sum;
    }
}

It can be seen that after changing to ordinary long type, unnecessary automatic packing process and instance creation process are reduced, and the time efficiency is about 10 times that of the original. Therefore, the conclusion is obvious: we should give priority to basic data types rather than packing basic types, and beware of unconscious automatic packing.



3. Summary

  • Avoid creating instead of not creating

Of course, don't mistakenly think that the cost of creating objects is very expensive. On the contrary, the constructor of small objects only does a small amount of display work, so the creation and recycling of small objects are very cheap, especially the modern JVM is highly optimized. It's actually a good thing to make the program clear and concise by creating additional objects.

  • Pool technology

Pooling technology is not necessary. Maintaining your own object pool through pooling technology is not necessarily useful to avoid creating objects, unless this object is very important. A typical example of correct use is database connection pool. Don't use pooling technology easily. Maintaining an object pool will certainly mess up the code, increase memory occupation, consume performance, and the efficiency is not necessarily faster than that of JVM automatic recycling. Modern JVMs have highly optimized garbage collectors, which can easily exceed the performance of lightweight object pools.

  • defensive copy

This aspect is not mentioned here. It is worth noting that when advocating the use of protective copies, the cost of reusing objects will be much greater than that of creating duplicate objects. If the protective copy is not implemented when necessary, it will lead to potential bugs and security vulnerabilities, and the creation of duplicate objects will only affect the style and performance of the program.






If there are any errors, please point them out

Topics: Java Back-end