10
头图

Hello, I am crooked.

A friend sent me this screenshot a few days ago:

He said he didn't understand why it didn't report an error like this.

I said that I didn't understand, how could I not report an error when assigning a boolean type to an int type, and then asked him: where did this code screenshot come from?

He said it was automatically generated by Lombok's @Data annotation.

Coincidentally, I knew a little about Lombok before, so the moment I heard this answer, I seemed to understand something: because Lombok uses bytecode enhancement technology to directly manipulate bytes code file, can it directly bypass the problem of variable type mismatch?

But soon I thought about it again, it's impossible: if this thing can be bypassed, Java is still playing a woolen thread.

So I decided to study it, and finally found that this is actually very simple: it is a bug of the idea.

recurrent

I used the Lombok plugin again, so I quickly reproduced it locally.

The source file is like this, I just added the @Data annotation:

The class file compiled by Maven install looks like this:

You can see that the @Data annotation helps us do a lot of things: generate a no-argument constructor, get/set methods for the name field, equals methods, toStrong methods and hashCode methods.

In fact, you click into the source code of the @Data annotation, it also explains to you, this is a composite annotation:

Therefore, the annotation that actually generates the hashCode method should be @EqualsAndHashCode.

So, in order to eliminate distractions and make it easier for me to focus on the hashCode method, I replaced the @Data annotation with @EqualsAndHashCode:

The result is still the same, just a lot less methods are generated by default, and I don't care about those methods.

Now, seeing is believing, why is the first line of code in the hashCode method here like this:

int PRIME = true;

Intuition tells me that there must be a trick here.

I first thought of another decompiler tool, jd-gui, and here it is:

Sure enough, after dragging the class file into jd-gui, the hashCode method is like this:

is the number 59, not true anymore.

But this PRIME variable doesn't seem to be useful in the hashCode method. Don't worry about this problem, throw it here first, and talk about it later.

Also, I thought of a way to view the bytecode directly:

You can see the integer push instruction bipush number 59 used in the first command of the hashCode method.

After the verification of jd-gui and bytecode, I have reason to suspect that int PRIME = true is displayed in the idea! right! Yes! BUG!

I'm happy, I found a bug again. Isn't the material here?

I was very happy at that time, just like the expression of the kid below.

clue

So I looked around on the Internet, but I didn't find any information on this, and I didn't get a little bit of gain. The inner OS is: "Ah, my posture must be wrong, do it again."

Expanded the search scope and searched again.

"Why are there still no clues? It doesn't make sense! No, there must be clues."

So look around again.

"Well, there's really no clue. Waste my hours, trash, that's all."

I exhausted all my life's learning and searched the Internet, but I really didn't find a line of code about why the idea is displayed here int PRIME = true .

The only relevant question I've found is this:

https://stackoverflow.com/questions/70824467/lombok-hashcode-1-why-good-2-why-no-compile-error/70824612#70824612

In this question, the buddy who asked the question said, why did he see the code int result = true and there was no compilation error?

Similar to what I saw, but not exactly the same. I found that his Test class has no parameters, while my own UserInfo for testing has a name parameter.

So I also took a look at it without reference:

I have no problem here, it shows int result = 1 .

Then someone asked if it's because your Test class has no fields, let's take a look at a few fields.

When he added two fields, the compiled class file is the same as what I see:

But there is only one valid answer to this question:

The buddy of this answer said: You see the hashCode method like this, probably because of a problem with the tool you used to generate the bytecode.

Inside the tool you use, the boolean values true and false are represented by 1 and 0, respectively.

Some bytecode decompilers blindly translate 0 to false and 1 to true, which is probably what you're experiencing.

What this buddy wants to express is also: this is a bug of the tool.

Although I always feel that it is almost meaningless, let's not say where it is, let's not press the table, let's look at it first.

In this answer, I also mentioned a feature of lombok, delombok. I want to talk about this first:

delombok

What is this?

Let me tell you a scenario, suppose you like to use Lombok's annotations, so you use the relevant annotations in the api package you provide externally.

But the classmate who quoted your api package does not like Lombok annotations, and has not done related dependencies and configurations, so the api package you provided in the past will definitely not be used by others.

So what to do?

That's where delombok comes in handy.

You can directly generate java source code that has parsed lombok annotations.

The description on the official website is as follows:

https://projectlombok.org/features/delombok

In other words, you can use it to see what the java file generated by lombok looks like for you.

I'll show you what it looks like.

From the description on the official website, you can see that delombok has many different opening methods:

For us, the easiest solution is to use the maven plugin directly.

https://github.com/awhitford/lombok.maven

Just paste this configuration into the pom.xml of the project.

But it should be noted that there is a paragraph below this configuration, and the first sentence at the beginning is very important:

Place the java source code with lombok annotations in src/main/lombok (instead of src/main/java).

Put the java source code annotated with lombok in the src/main/lombok path instead of src/main/java.

So, I created a lombok folder and moved the UserInfo.java file into it.

Then execute the maven install operation, you can see that there is an additional UserInfo.java file under the target/generated-sources/delombok path:

This file is the java file processed by the delombok plug-in. It can be directly put into the api and provided when the other party does not use the lombok plug-in.

Then we take a look at this file. I got this file mainly to see what its hashCode method looks like:

See no, the hashCode method int PRIME = true is gone, replaced by final int PRIME = 59 .

This is already a java file. If this place is still true, then a proper compilation error:

And the source code generated by delombok also answered one of my previous questions:

When looking at the class file, I feel that the variable PRIME has not been used, so what is the meaning of it?

But looking at the java file compiled by delombok, I know that PRIME is actually used:

So why did PRIME become true?

Looking at the source code generated by delombok, my eyes suddenly lit up, man, look what this is:

This is a local variable of type final.

Note: yes! final! kind! type!

In order to better elicit the probability that I want to say below, I will write you a very simple thing first:

See, why and mx have become true, which is equivalent to directly modifying the test method to this:

 public int test() {
    return 3;
}

It might be a bit more intuitive to show you the bytecode:

The left is without final, and the right is with final.

It can be seen that after adding final, there is no iload operation to access local variables at all.

What is this thing called?

This is "constant folding".

I was fortunate to have seen the interpretation of this phenomenon by the JVM boss R a long time ago. I thought it was very interesting at the time, so I was a little impressed.

When I saw final int PRIME = 59 , the memories were ignited.

So I found the link I saw earlier:

https://www.zhihu.com/question/21762917/answer/19239387

In R's answer, there is such a short paragraph, I will show you a screenshot:

At the same time, let me show you the definition of constant variable in the Java language specification:

A variable of primitive type or type String, that is final and initialized with a compile-time constant expression , is called a constant variable.

A variable of basic type or String type, if it is final modified, is initialized at compile time, which is called constant variable (constant variable).

So final int PRIME = 59 the PRIME in it is a constant variable.

Since String is mentioned here, let me give you an example:

Look at the test2 method, which uses final. In the final class file, the string after the splicing is returned directly.

why?

Don't ask, ask is the rule.

https://docs.oracle.com/javase/specs/jls/se7/html/jls-15.html#jls-15.28

I'm just here to show you the way, you can go through it yourself if you are interested.

In addition, I also hammered the following display in the class file again. It is indeed a bug of the idea, and it has nothing to do with lombok, because I don't use lombok at all here:

At the same time, there are also related discussions on the above issue in lombok's github:

https://github.com/projectlombok/lombok/issues/523

The questioner said: This PRIME variable looks like useless code because it is not used in this local method.

The official answer is: Brother, I suspect that what you are seeing is an optimization of javac. If you look at the code generated by delombok, you will see that the PRIME thing is being used. It should be that javac has inlined this constant.

why 59

Let's focus again on the hashCode method generated by delombok:

Why is 59 used here, shouldn't the factor in hashCode be 31 for no-brainer?

I thought there was a story here, so I dug a little further.

This is how I dig for clues.

First of all, let me find out how the number 59 came from. It must come from a file in lombok.

Then I pulled down the source code of lombok and checked the commit or change for this value in the corresponding file. Under normal circumstances, this magic value will not come for no reason. When submitting the code, there is a high probability that an explanation will be given on why this value is taken.

I just need to find that description.

First, I found this class based on where @EqualsAndHashCode is called:

lombok.javac.handlers.HandleEqualsAndHashCode

Then in this class, we can see the familiar "PRIME":

Then, searching for this keyword, I found this place:

This method here is where 59 comes from:

lombok.core.handlers.HandlerUtil#primeForHashcode

Even if the first step is completed, then we have to look at the submission record of the HandlerUtil class in lombok:

The result is very smooth, the commit information of the second submission of this class is saying why it is not useful 31.

From the commit information, 31 should be used before, and the reason for using this number is because "Effective Java" recommends it. But according to the opinion in issue#625, maybe 277 is a better value.

It can also be seen from the submitted code that 31 was indeed used before, and it was directly written to death:

In this commit, it was modified to 277 and mentioned in a constant of HandlerUtil:

However, this is not the 59 I was looking for, so I continued to look for it.

Soon, this change from 277 to 59 was found:

Also points to issue#625.

When I hummed and sang songs and was about to go to issue #625 to find out, I was dumbfounded:

https://github.com/projectlombok/lombok/issues/625

What issue#625 said has nothing to do with hashCode at all.

And this question was only raised on July 15, 2015, but the code was submitted in January 2014.

So lombok's issues must be missing a large part, so I can't match the number now.

This behavior belongs to poisoning in the code, and I am a poisoned person.

Things looked like they were walking into a dead end.

But soon, things turned around, because another place that might have an answer flashed through my little brain, and that was the changelog:

https://projectlombok.org/changelog

Sure enough, in the changelog, I found a new thread issue#660:

Open issue#660 and take a look, um, this time it should be right:

https://github.com/projectlombok/lombok/issues/660

In this issue, Maaartinus first gave a piece of code, and then he explained:

In my case, if the hashCode method generated by lombok uses a factor of 31, there are only 64 unique hashes for the 256 generated objects, which means a lot of collisions.

But if lombok uses a better factor, the number increases to 144, which is relatively good.

And almost any odd number will do. Using 31 is one of the few poor choices.

After the official saw it, they quickly replied:

After reading the program of my brother, I think what my brother said makes sense. I used 31 before because it was suggested in Effective Java, and I didn't think about it too much.

Also, I decided to use the number 277 instead of 31 as the new factor.

Why 277?

Don't ask, just ask is it's very lucky!

277 is the lucky winner

So why was it changed from 277 to 59 in the end?

Because of using a "huge" factor like 227, there is about a 1-2% performance penalty. So need to change a number.

The final decision was to choose 59, although no specific reasons were given:

But looking at the changelog, I have reason to guess that one of the reasons is to choose a number less than 127, because -128 to 127 are in the cache range of Integer:

IDEA

Speaking of IDEA's BUG, I stepped on an impressive "BUG" in my early years.

I used to debug the ConcurrentLinkedQueue thing, and I just broke my mentality.

There is a huge pit you may encounter. For example, our test code is like this:

 public class Test {

    public static void main(String[] args) {
        ConcurrentLinkedQueue<Object> queue = new ConcurrentLinkedQueue<>();
        queue.offer(new Object());
    }
}

Very simple, add an element to the queue.

Due to the case of initialization head=tail=new Node<E>(null):

So the item pointer in the linked list structure after the add method is called should look like this:

We add a few output statements to the offer method:

The log after execution is as follows:

Why is the last line of output, the log output after [offer] not null->@723279cf?

Because this method will call the first method to get the real head node, that is, the node whose item is not null:

Everything is fine up to here. However, it's different when you operate in debug mode:

The item of the head node is not null anymore! The next node of the head node is null, so a null pointer exception is thrown.

In the case of single thread, the result of running the code directly is inconsistent with the result of Debug running ! Isn't this a ghost?

I checked the Internet and found that there are quite a few netizens who have encountered ghosts.

Finally found this place:

https://stackoverflow.com/questions/55889152/why-my-object-has-been-changed-by-intellij-ideas-debugger-soundlessly

This guy has the exact same problem as us:

There is only one answer to this question:

Do you know who the dude who answered this question is?

IDEA's product manager, with my respect.

The final solution is to close these two configurations of IDEA:

Because IDEA will actively help us to call the toString method once in Debug mode, and in the toString method, it will call the iterator.

The iterator of CLQ will trigger the first method, which, as mentioned before, will modify the head element:

Everything has come to light.

And the question in this article:

I'm reasonably sure it's an IDEA problem, but I haven't been able to find a certification from an authoritative person like the problem in this subsection.

So that's what I almost meant earlier.

--- This article was first published on the public account why technology.


why技术
2.2k 声望6.8k 粉丝