Using var to declare variables, there are so many details in java, do you know?

Introduction

Java SE 10 introduced type inference for local variables. Earlier, all local variable declarations had to declare clear types on the left. Using type inference, some explicit types can be replaced with the reserved type var of local variables with initialized values. This type of var as the type of local variables is inferred from the type of the initialized value.

There is some controversy regarding this feature. Some people welcome its conciseness, while others worry that it deprives readers of the type information that is valued by readers, thereby compromising the readability of the code. Both views are correct. It can make the code more readable by eliminating redundant information, and it can also reduce the readability of the code by deleting useful information. Another point is that it is worried that it will be abused, leading to writing worse Java code. This is also true, but it may also lead to writing better code. Like all functions, use it requires judgment. There is no set of rules for when to use and when not to use.

Local variable declarations do not exist in isolation; surrounding code can affect or even overwhelm the impact of using var. The purpose of this document is to examine the impact of surrounding code on var declarations, explain some of the trade-offs, and provide guidelines for the effective use of var.

Principle of Use

P1. Reading code is more important than writing code

Code reading frequency is much higher than writing code. In addition, when writing code, we usually have to have an overall idea, and it takes time; when reading code, we often browse in context, and may be more hurried. Whether and how to use a specific language feature should depend on its impact on future readers, not its author. Shorter programs may be preferable to longer ones, but abbreviating the program too much will omit information useful for understanding the program. The core issue here is to find the right size for the program in order to maximize the readability of the program.

Here we pay special attention to the amount of codewords required when entering or writing programs. Although conciseness may be a good encouragement to the author, focusing on it will ignore the main goal, which is to improve the readability of the program.

P2. After local variable type inference, the code should become clear

Readers should be able to view var declarations and immediately understand what is happening in the code when using local variable declarations. Ideally, the code can be easily understood only through code snippets or context. If reading a var statement requires readers to look at several locations around the code, using var at this time may not be a good situation. Moreover, it also indicates that there may be a problem with the code itself.

P3. The readability of the code should not depend on the IDE

The code is usually written and read in the IDE, so it is easy to rely on the code analysis function of the IDE. For type declarations, using var anywhere, you can use the IDE to point to a variable to determine its type, but why not do this?

There are two reasons. The code is often read outside the IDE. The code appears in many places where IDE facilities are not available, such as snippets in documents, browsing repositories or patch files on the Internet. In order to understand the role of these codes, you must import the code into the IDE. This is counterproductive.

The second reason is that even when reading the code in the IDE, it usually requires a clear operation to query the IDE to obtain more information about the variables. For example, to query the variable type declared with var, you may have to hover the mouse over the variable and wait for a pop-up message. This may only take a moment, but it will disrupt the reading process.

The code should be presented automatically, its surface should be understandable, and without the help of tools.

P4. Explicit typing is a trade-off

Java has always required explicit types in local variable declarations. Obviously, explicit types can be very useful, but they are sometimes not very important, and sometimes they can be ignored. Asking for a clear type may also mess up some useful information.

Omitting explicit types can reduce this confusion, but only if this confusion does not damage its intelligibility. This type is not the only way to convey information to readers. Other methods including variable names and initialization expressions can also convey information. When determining whether one of the channels can be muted, we should consider all available channels.

Guideline G1. It is usually a good practice to choose a variable name that provides useful information, but it is more important in the context of var. In a var declaration, the name of the variable can be used to convey information about the meaning and usage of the variable. The use of var to replace explicit types also improves the names of variables. E.g:

//原始写法
List<Customer> x = dbconn.executeQuery(query);

//改进写法
var custList = dbconn.executeQuery(query);

In this case, the useless variable name x has been replaced with a name custList that evokes the variable type, which is now implicit in the var declaration. According to the logic result of the method, the variable type is coded, and the variable name custList in the form of "Hungarian notation" is obtained. Just like explicit typing, this is sometimes helpful, sometimes just messy. In this example, the name custList means that List is being returned. This may also be unimportant. Unlike explicit types, variable names can sometimes better express the role or nature of the variable, such as "customers":

//原始写法    
try (Stream<Customer> result = dbconn.executeQuery(query)) {
     return result.map(...)
                  .filter(...)
                  .findAny();
 }
//改进写法
try (var customers = dbconn.executeQuery(query)) {
    return customers.map(...)
                    .filter(...)
                    .findAny();
}

G2. Minimizing the scope of use of local variables Minimizing the scope of local variables is usually a good habit. This approach is described in Effective Java (Third Edition), item 57. If you want to use var, it will be an additional boost.

In the following example, the add method correctly adds the special item to the last element of the list collection, so it is processed last as expected.

var items = new ArrayList<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...

Now suppose that in order to delete duplicate items, the programmer wants to modify this code to use HashSet instead of ArrayList:

var items = new HashSet<Item>(...);
items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...

This code now has a bug because Set does not define the iteration order. However, the programmer may fix this bug immediately because the use of the items variable is adjacent to its declaration.

Now suppose this code is part of a big method, and accordingly the items variable has a wide range of use:

var items = new HashSet<Item>(...);

// ... 100 lines of code ...

items.add(MUST_BE_PROCESSED_LAST);
for (var item : items) ...

The impact of changing ArrayList to HashSet is no longer obvious, because the code that uses items is far away from the code that declares items. This means that the bug mentioned above seems to survive longer.

If items have been clearly declared as List, you also need to change the initializer to change the type to Set. This may prompt the programmer to check the rest of the method for code affected by such changes. (If the problem comes, he may not check). If you use var will eliminate such effects, but it may also increase the risk of introducing errors in such code.

This seems to be an argument against the use of var, but it is not. The initialization procedure using var is very streamlined. This problem only occurs when the use of the variable is large. You should change the code to reduce the scope of local variables and declare them with var instead of simply avoiding var in these situations.

G3. When the initialization program provides enough information for the reader, please consider using var local variables are usually initialized in the constructor. The name of the constructor being created usually duplicates the type explicitly declared on the left. If the type name is very long, you can use var to improve conciseness without losing information:

// 原始写法:
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
// 改进写法
var outputStream = new ByteArrayOutputStream();

In the case where the initializer is a method call, it is reasonable to use var, such as a static factory method, and its name contains enough type information:

//原始写法
BufferedReader reader = Files.newBufferedReader(...);
List<String> stringList = List.of("a", "b", "c");
// 改进写法
var reader = Files.newBufferedReader(...);
var stringList = List.of("a", "b", "c"); 

In these cases, the name of the method strongly implies its specific return type, which is then used to infer the type of the variable.

G4. Use var local variables to decompose chained or nested expressions. Consider using a collection of strings and find the code of the most frequently occurring string, which may look like this:

return strings.stream()
               .collect(groupingBy(s -> s, counting()))
               .entrySet()
               .stream()
               .max(Map.Entry.comparingByValue())
               .map(Map.Entry::getKey);

This code is correct, but it can be confusing because it looks like a single stream pipeline. In fact, it is a short stream first, then the second stream generated by the result of the first stream, and then the stream after the optional result of the second stream is mapped. The most readable way to express this code is two or three sentences; the first group of entities is put into a Map, then the second group is filtered out of this Map, and the third group is extracted from the Map result by Key, as shown below:

Map<String, Long> freqMap = strings.stream()
                                   .collect(groupingBy(s -> s, counting()));
Optional<Map.Entry<String, Long>> maxEntryOpt = freqMap.entrySet()
                                                       .stream()
                                                       .max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);

But the writer may refuse to do this because it seems too cumbersome to write the types of intermediate variables, on the contrary they tampered with the control flow. Using var allows us to express code more naturally without the high cost of explicitly declaring the types of intermediate variables:

var freqMap = strings.stream()
                     .collect(groupingBy(s -> s, counting()));
var maxEntryOpt = freqMap.entrySet()
                         .stream()
                         .max(Map.Entry.comparingByValue());
return maxEntryOpt.map(Map.Entry::getKey);

Some people may prefer a single long chain of calls in the first piece of code. However, under certain conditions, it is better to break down long method chains. Using var for these situations is a viable method, and using the full declaration of the intermediate variable in the second paragraph would be a bad choice. As in many other situations, correct use of var may involve throwing things away (display types) and adding things (better variable names, better code structure).

G5. Don't worry too much about the use of local variables in "Programming with Interfaces"

A common idiom in Java programming is to construct instances of specific types, but assign them to variables of interface types. This binds the code to the abstract rather than to the concrete implementation, and retains flexibility for future maintenance of the code.

//原始写法, list类型为接口List类型
List<String> list = new ArrayList<>()

If you use var, you can infer that the specific implementation type of list is ArrayList instead of the interface type List.

// 推断出list的类型是 ArrayList<String>.
var list = new ArrayList<String>(); 

To repeat here, var can only be used for local variables. It cannot be used to infer attribute types, method parameter types, and method return types. The principle of "programming with interfaces" remains as important in these situations as ever.

The main problem is that the code that uses this variable can form a dependency on the specific implementation. If the initialization procedure of the variable is to be changed in the future, this may cause its inferred type to change, resulting in an exception or bug in the subsequent code that uses the variable.

If, as suggested in Guide G2, the scope of local variables is small, the "vulnerabilities" that may affect the specific implementation of subsequent codes are limited. If the variable is only used for a few lines of code, it should be easy to avoid these problems or alleviate these problems.

In this special case, ArrayList only contains some methods that are not on List, such as ensureCapacity() and trimToSize(). These methods will not affect the List, so calling them will not affect the correctness of the program. This further reduces the impact of inferred types as concrete implementation types rather than interface types.

G6. Be careful when using var with <> and generic methods

The var and <> functions allow you to omit specific type information when you can derive from existing information. Can you use them in the same variable declaration?

Consider the following code:

PriorityQueue<Item> itemQueue = new PriorityQueue<Item>();

This code can be rewritten using var or <> without losing type information:

// 正确:两个变量都可以声明为PriorityQueue<Item>类型
PriorityQueue<Item> itemQueue = new PriorityQueue<>();
var itemQueue = new PriorityQueue<Item>();   

It is legal to use var and <> at the same time, but the inferred type will change:

// 危险: 推断类型变成了 PriorityQueue<Object>
var itemQueue = new PriorityQueue<>();

From the above inference results, <> can use the target type (usually on the left side of the declaration) or constructor as the parameter type in <>. If neither exists, it will go back to the broadest applicable type, usually Object. This is usually not what we expected.

Generic methods have already provided type inference. The use of generic methods rarely requires explicit type parameters. If the actual method parameters do not provide sufficient type information, the inference of generic methods will depend on the target type. There is no target type in the var declaration, so similar problems can occur. E.g:

// 危险: list推断为 List<Object>
var list = List.of();

Using <> and generic methods, other type information can be provided through the actual parameters of the constructor or method, allowing the expected type to be inferred, thus:

// 正确: itemQueue 推断为 PriorityQueue<String>
Comparator<String> comp = ... ;
var itemQueue = new PriorityQueue<>(comp);

// 正确: infers 推断为 List<BigInteger>
var list = List.of(BigInteger.ZERO);

If you want to use var with <> or generic methods, you should ensure that the method or constructor parameters provide enough type information so that the inferred type matches the type you want. Otherwise, please avoid using var and <> or generic methods in the same declaration.

G7. Be careful when using basic types of var

Basic types can be initialized using var declarations. Using var in these situations is unlikely to provide many advantages, because the type name is usually short. However, var is sometimes useful, for example, to align the names of variables.

The basic types of boolean, character, long, and string use var. The inference of these types is accurate because the meaning of var is clear.

// 原始做法
boolean ready = true;
char ch = '\ufffd';
long sum = 0L;
String label = "wombat";

// 改进做法
var ready = true;
var ch    = '\ufffd';
var sum   = 0L;
var label = "wombat";

When the initial value is a number, you should be especially careful, especially the int type. If there is a display type on the left, then the right will silently convert the int value to the corresponding type on the left by up or down conversion. If var is used on the left, then the value on the right will be inferred to be of type int. This may be unintentional.

// 原始做法
byte flags = 0;
short mask = 0x7fff;
long base = 17;

// 危险: 所有的变量类型都是int
var flags = 0;
var mask = 0x7fff;
var base = 17;  

If the initial value is a float, the inferred type is mostly unambiguous:

// 原始做法
float f = 1.0f;
double d = 2.0;

// 改进做法
var f = 1.0f;
var d = 2.0;

Note that the float type can be silently upcast to the double type. Using an explicit float variable (such as 3.0f) to initialize a double variable can be a bit slow. However, if you use var to initialize double variables with float variables, you should pay attention to:

// 原始做法
static final float INITIAL = 3.0f;
...
double temp = INITIAL;

// 危险: temp被推断为float类型了
var temp = INITIAL;  

(In fact, this example violates the G3 guidelines, because the initialization program does not provide enough type information to let the reader understand its inferred type.)

Example

This section contains some examples where var can be used to achieve good results.

The following code represents the removal of matching entities from a Map based on the maximum number of matches max. Wildcard (?) type boundary can improve the flexibility of the method, but the length will be very long. Unfortunately, the Iterator type here is also required to be a nested wildcard type, which makes its declaration more verbose, so that the length of the for loop title cannot fit on one line. It also makes the code more difficult to understand.

// 原始做法
void removeMatches(Map<? extends String, ? extends Number> map, int max) {
    for (Iterator<? extends Map.Entry<? extends String, ? extends Number>> iterator =
             map.entrySet().iterator(); iterator.hasNext();) {
        Map.Entry<? extends String, ? extends Number> entry = iterator.next();
        if (max > 0 &amp;&amp; matches(entry)) {
            iterator.remove();
            max--;
        }
    }
}  

Using var here can delete some interfering type declarations for local variables. In this type of loop, the explicit type Iterator, Map.Entry is largely unnecessary. Using the var declaration can put the for loop title on the same line. The code is also easier to understand.

// 改进做法
void removeMatches(Map<? extends String, ? extends Number> map, int max) {
    for (var iterator = map.entrySet().iterator(); iterator.hasNext();) {
        var entry = iterator.next();
        if (max > 0 &amp;&amp; matches(entry)) {
            iterator.remove();
            max--;
        }
    }
}

Consider using the try-with-resources statement to read a single line of text from the socket. Network and IO classes are generally decorative classes. When in use, each intermediate object must be declared as a resource variable so that the resource can be closed correctly when opening the subsequent decoration class. Conventional writing code requires repeated class names to be declared around variables, which can cause a lot of confusion:

// 原始做法
try (InputStream is = socket.getInputStream();
     InputStreamReader isr = new InputStreamReader(is, charsetName);
     BufferedReader buf = new BufferedReader(isr)) {
    return buf.readLine();
}  

Using var declarations will reduce a lot of this confusion:

// 改进做法
try (var inputStream = socket.getInputStream();
     var reader = new InputStreamReader(inputStream, charsetName);
     var bufReader = new BufferedReader(reader)) {
    return bufReader.readLine();
}

Conclusion

Using var declarations can improve the code by reducing clutter, allowing more important information to stand out. On the other hand, using var indiscriminately can also make things worse. Used properly, var can help improve good code, making it shorter and clearer, without compromising comprehensibility.

Reply to "information" by private message to receive a summary of Java interview questions from a major manufacturer + Alibaba Taishan manual + a guide to learning and thinking of each knowledge point + a summary of Java core knowledge points in a 300-page pdf document!

The content of these materials are all the knowledge points that the interviewer must ask during the interview. The chapter includes many knowledge points, including basic knowledge, Java collections, JVM, multi-threaded concurrency, spring principles, microservices, Netty and RPC, Kafka , Diary, design pattern, Java algorithm, database, Zookeeper, distributed cache, data structure, etc.

file

Guess you like

Origin blog.csdn.net/weixin_46577306/article/details/107800101