Optimizing equals() Methods with Pattern Matching
Optimizing your equals() methods with Pattern Matching - JEP Cafe #21
Estimated read time: 1:20
Summary
In this episode of JEP Cafe, the focus is on optimizing the equals() methods in Java using pattern matching. The creator delves into the pitfalls of relying solely on IDE-generated methods, and how potentially better-performing patterns exist that developers may not be aware of. The session explains the importance of well-designed equals() and hashCode() methods, especially when considering performance impacts. Pattern matching, instance checking, and predictive branching are explored as strategies for optimizing these methods. Various examples and benchmarks show that, in many cases, traditional practices might lead to performance hits, and a more nuanced approach could yield better results. The creator emphasizes measuring performance accurately for specific use cases as crucial for making the right optimization choices.
Highlights
- Using pattern matching can lead to more efficient equals() methods. 📊
- Traditional instance checks can sometimes slow down performance. ⏳
- Understanding and leveraging JEP 394 can optimize code significantly. 🤓
- Measure your specific performance needs before deciding on an optimization strategy. ⚖️
- Predictive branching can cause unexpected performance hits if not accounted for. 🚦
Key Takeaways
- Don't just rely on your IDE for equals() and hashCode() methods, explore pattern matching for optimization! 🌟
- A well-designed hashCode() is crucial for performance, especially avoiding common pitfalls like returning constant values. 🚀
- Instance checking isn't always beneficial; in some cases, it can degrade performance, especially with modern setups. 🎯
- Predictive branching impacts performance; understanding it can help optimize equals() designs. 🔍
- Pattern matching can outperform traditional approaches, especially with the recent Java advancements. ☕
Overview
In this engaging episode of JEP Cafe, the creator takes us through the intricacies of optimizing equals() methods in Java, shedding light on the often-overlooked performance impacts that poorly designed methods can have. The journey begins with a deeper look into the equals() and hashCode() methods — the foundational companions to any Java class.
The discussion highlights the power of pattern matching introduced in JEP 394 and how it can be leveraged effectively to write performant Java code. We're guided through practical examples, comparing traditional instance checks with the modern, streamlined approaches pattern matching offers. The benchmarks provided show compelling evidence for reconsidering the default code we might take for granted.
Amidst the Java jibber-jabber, the creator brings out a critical message: the importance of measuring performance based on real-world use cases. As technology evolves, so too should our approaches. The episode wraps up with a call to rethink old patterns and embrace more efficient options, like those available in Java 21 with pattern matching, to ensure your Java applications run smoother and faster.
Chapters
- 00:00 - 01:00: Introduction to equals() Methods The chapter titled 'Introduction to equals() Methods' begins with an engaging opening where the speaker humorously asks if the audience is interested in a coffee break. This sets a casual and relatable tone for the discussion that follows. The chapter is focused on the design aspects of equals() methods in programming. It emphasizes their pervasive use in software development, pointing out that these methods are often utilized even without developers being fully aware of it. This introduction sets the stage for a deeper exploration into best practices and considerations when implementing equals() methods.
- 01:00 - 03:00: Generating equals() and hashCode() in IDEs The chapter discusses the practice of using IDEs (Integrated Development Environments) to generate the equals() and hashCode() methods in programming. It argues against manually writing these methods due to the tediousness and suggests that IDEs can automate this task effectively. The chapter further explores whether IDEs produce optimal code patterns or if there are better alternatives. It promises to address these questions and more, including concepts like performance optimization, instance checking, pattern matching with instanceof, and predictive branching, using JMH benchmarks.
- 03:00 - 05:00: Understanding HashSet and HashCode The chapter titled 'Understanding HashSet and HashCode' focuses on the significance and implementation of the equals() and hashCode() methods in programming. It emphasizes the importance of understanding these methods for designing simple and efficient equals() methods. The chapter also touches upon the common practice of generating these methods using an Integrated Development Environment (IDE), alongside the toString() method. While automatic generation is beneficial, especially for the hashCode() method, having a deep understanding of their workings can greatly enhance code performance.
- 05:00 - 09:00: Performance Issues with equals() Methods The chapter discusses the challenges of writing effective hashCode() methods. It emphasizes the complexity involved in creating a good hashCode() and warns about the unexpected negative impacts a poorly implemented hashCode() can have on application performance. The chapter advises using generated hashCode() methods unless the user is experienced enough to handle custom implementations and begins with a practical example to illustrate these points.
- 09:00 - 14:30: Impact of Predictive Branching This chapter discusses the potential pitfalls of implementing a poorly designed hashCode() function in programming. The example given illustrates a point record with a default hashCode() provided by JDK and contrasts it with a 'dumb' implementation that always returns 0. This highlights the importance of designing efficient and sensible hashing methods.
- 14:30 - 29:00: Testing equals() Methods with Various Data Sets In this chapter, the focus is on testing the equals() method across various data sets. It highlights the often overlooked but essential rule in Java that if two objects are considered equal by the equals() method, they must have the same hash code. This ensures consistent behavior when these objects are stored in hash-based collections like HashSet. The chapter further explores the practical implementation of this rule by using Java 19's newHashSet() factory method, which simplifies the creation of HashSet by determining the necessary initial capacity to store all incoming objects efficiently. This discussion serves as a reminder that while certain implementation tricks might appear unconventional or redundant, they are crucial for aligning with Java's specifications and ensuring robust application performance.
- 29:00 - 32:00: Conclusion: Importance of Measuring Performance In the concluding chapter titled 'Conclusion: Importance of Measuring Performance', the importance of evaluating performance in programming is highlighted. The discussion involves measuring the execution time of a piece of code involving a `Point` class with a default `hashCode()` implementation compared to a poorly implemented one. The results demonstrate a significant performance deterioration, epitomized by a 20-fold performance decline when using a poorly crafted hash code, underscoring the necessity of efficient coding practices.
- 32:00 - 35:00: Evaluating IDE-Generated Code The chapter discusses the inner workings of a HashSet, specifically how it wraps around a HashMap. It explains that a HashMap begins by computing the hash code for each key to determine the storage bucket. The chapter also covers the issue of hash code collisions, which can result in a linked list that may evolve into a red-black tree if it becomes excessively long.
- 35:00 - 33:00: Final Thoughts on Pattern Matching in Java 21 The chapter emphasizes the complexity and performance considerations involved in managing code, particularly when using records and classes in Java 21. It advises on the best practices such as avoiding manual alteration of records and relying on integrated development environments (IDEs) to automatically generate hashCode() methods for plain classes to ensure safety and efficiency.
Optimizing your equals() methods with Pattern Matching - JEP Cafe #21 Transcription
- 00:00 - 00:30 Do you fell like a coffee break? So do I! What about we talk about the design of your equals() methods. Methods that you use all the time, even when you don't know it,
- 00:30 - 01:00 and that you never write yourself. First because it's boring, and second because your IDE can do it for you. Is your IDE good at generating this method? Are there better patterns of code than the one it gives you? These are the questions I would like to answer in this JEP Café. So, you will learn about performance, instance checking, pattern matching for instanceof, predictive branching, all these good things with JMH benchmarks,
- 01:00 - 01:30 that, once again, will help you design simple and performant equals() methods. The equals() and the hashCode() methods are probably the first methods you learn to generate in your IDE, along with the toString() method, of course. And, dont get me wrong, generating them is a good thing, especially for the hashCode() method,
- 01:30 - 02:00 because writing a good hashCode() method is actually not that easy. And because it is used in places where you may not expect it, a bad hashCode() implementation can lead to major performance hits in your application. So yes, you should definitely use this generated hashCode() method, unless you know what you are doing. I just would like to start with a first example
- 02:00 - 02:30 to show you what could go wrong with a dumb hashCode() implementation. And when I say dumb... Suppose you have a point record, plain and simple. You do not touch this class, so the hashCode() implementation is provided by the JDK. Good. And then, let us write this completely dumb implementation of a hashCode() method, that always return 0.
- 02:30 - 03:00 I know, it's completely stupid, never do that in your application, but this implementation is actually OK with the spec: two points that are equal, do have the same hash code. Let us try to put these points in a HashSet, along with the following code. And bytheway, we are using this factory method newHashSet(), that was added in Java 19, so, that you can use it in 21. It creates a large enough hash set to hold all your values, without having to
- 03:00 - 03:30 resize it on the fly. Neat, I love this one. So let us measure the time it takes to run this code in both cases: the Point with the default hashCode() implementation, and the dumb Point that has this stupid one. And as you can see, the performance hit is quite hard! That's a factor of 20 between a good hash code and a stupid hash code.
- 03:30 - 04:00 Why is that? Well, as you probably know, a HashSet is actually wrapping a HashMap. And a HashMap first computes the hash code of your key to decide in which bucket it will store your entry. If all the hash codes are the same, then you will get collisions, leading to the construction of linked list, that will become a red black tree, if this list is too long.
- 04:00 - 04:30 This is a code that is much more complex and costly to run than the simple storing of an entry in an array. Thus the performance hit. So I hope this example convinced you: if you are using records, do not touch anything, and for your plain classes, let your IDE generate your hashCode() method for you. It's much safer, most of the time.
- 04:30 - 05:00 Let us go back to our initial subject, and talk about the equals() method. There is actually a number of generators that you can use, and I would like to first take a look at the code they give you, to compare these patterns. In all the rest, we are going to use our good friend the record Point, with two components x and y, of type int. This is a very simple example, and comparing two instances of this record is quite fast: it's only about comparing two int values,
- 05:00 - 05:30 which is, yes, fast. You may have objects in your application, with more complex states, that will be more costly to compare. First, we have the default generators of the Eclipse IDE and the IntelliJ IDE, which are slightly different. Here is the generated code by both of them. And, as you can see, it's mostly the same,
- 05:30 - 06:00 organized in a different way. It starts with the comparison of this and other with ==, to quickly exit if the objects are actually the same. This is an instance check, and is seen as an optimization that almost all the generators are using. Then it compares the classes, also with ==, because you know that there is only one instance of a given class in your application. And then the cast and the comparison of the internal state of your objects.
- 06:00 - 06:30 Then you can also use the Guava generator, which is also very similar, just organized in a different way. And you can see that all these generated codes compare the instances with ==, then the classes, then they do a cast, and compare the internal state of your objects. And then you can check the Apache Commons Lang 3 generator. The code is organized in the same way: first compare the instances,
- 06:30 - 07:00 then the class, and then the internal state of your object, but this time using a builder. On the one hand, I find this code nice and readable, but on the other hand, I suspect that it will not be great performance wise. Creating this builder is costly, much more costly than comparing primitive types with ==. If performance is not an issue for you, and you want to favor the readability of your code, this is definitely a solution that you can consider.
- 07:00 - 07:30 Other than that, I'm not sure that this solution is very interesting. And because we also need to check your basic 101 equals() method, we are going to a plain instanceof to the lot, and the same, with this instance check optimization first. And because this is Java 21 we are going to add two more.
- 07:30 - 08:00 First the plain equals() method that is present in your records. You do not see this implementation because it is added for you by the compiler, but it's still there. And then the implementation advertised in the JEP about Pattern Matching for instanceof. This is the JEP 394, delivered in the JDK 16, and written by Gavin Bierman. We can take a look at this implementation now, because it's really interesting.
- 08:00 - 08:30 As you can see, there is one major difference between this implementation and all the others, including bytheway the default implementation you currently get when you create a record, which is: it does not check if the instances are the same or not with ==. And it does not check if the classes are the same or not neither. It directly checks for the pattern.
- 08:30 - 09:00 Could this be a mistake? A missing optimization, overlooked by Gavin Bierman that wrote the JEP? Or is it actually the right way to write your equals() method? So let us add this method from the JEP, and also modify it by adding this instance check before, to see if this is an optimization or not. And, bytheway, we are going to use the Record Pattern, because this is what we have in 21, and because our Point class is a record.
- 09:00 - 09:30 The most important question is: is this instance check with == an optimization or not? And if it is really an optimization, why does the code in the JEP doesn't use it? Why would it give up on what looks like an obvious optimization? Well, you know the drill, right? When it comes to performance, there is only one principle to follow. Measure, don't guess! Before we move on, and examine all this, I
- 09:30 - 10:00 would like to talk about a very important point in computing, which is called predictive branching. You may be wondering why we are taking about that now. In fact, it turns out that it actually plays a key role in all this, so we are going to spend a few minutes to explain what it is. Predictive branching is a nifty feature that may make the execution of your tests
- 10:00 - 10:30 much faster. It's not that it doesn't execute your test, because it does, it needs to execute it. It's more that, it makes a bet on the outcome of this test, starts to execute one of the branch of your if statement, and if the bet is a win, carries on with your code. Which is great, because if it works, then it can execute your code much faster.
- 10:30 - 11:00 But if it doesn't, then it becomes a performance hit, because it needs to rollback what it did and take the other branch. So let us see some benches on this topic. First, let us run this code. We are just summing all the int values of an array of size 400. There is no test in it, and it runs in about 72ns "on my machine". Then we are going to suppose that all the elements of this array are even integers,
- 11:00 - 11:30 and we just add a test on that. Note that the result of this test is always true. This gives us an idea of the cost of testing each element. On the overall, running this code takes 110ns "on my machine", which is about 50% more than not running the test. Now we are going to run three more benches, to evaluate the cost of a failing test. Let us make the last element of this array
- 11:30 - 12:00 an odd integer. So the last test of the series is failing, when all the other tests are a success. And you can see that having just one test that is failing, when all the others are successful, is actually quite costly. You could expect that the performance should be the same, because it is the same test, just a different outcome. And we are executing 400 tests
- 12:00 - 12:30 in 110ns, so the throughput of this code is about 0,27ns per operation. But because the result is not what the optimizer expected, the performance hit for this single failing test is about 7ns. Which is quite a big performance hit. This single failing test is about 30 times slower
- 12:30 - 13:00 than all the others. This is the cost of a failing predictive branching. Now, let us move this failing test from the end of the array to the middle of the array. And you can see that the performance hit this time is almost doubled. It is now about 13ns instead of 7. It looks like we are paying for the transition from one state to the other.
- 13:00 - 13:30 The first state being: the expected outcome of the next test is true, and the second state being: the expected outcome is false. We can see that by running the same bench with two consecutive odd integers at the middle of the array instead of one. If we measure this performance, we can see that it is the same as the one where we have only one odd integer, about 123ns. That confirms it: what is coslty is to fail the
- 13:30 - 14:00 predictive branching optimization, that is, to transition from one state to the other. Now we can try one last test: having two odd integers spread apart in our array. One at the first third, and another one at the second third. And you can see that the performance hit this time is even higher. Not as high as expected,
- 14:00 - 14:30 if we were paying for transitions the performance hit would have been 138ns, and it's only 128ns, but still higher than having two odd consecutive integers. So this gives you an idea of the cost of having a test with an outcome that is not what this "predictive branching" feature would have expected.
- 14:30 - 15:00 Actually testing elements and having an outcome that is completely random is costly, you should try to avoid being in such a situation, when you can. Now the question is: how is this related to the design of your equals() methods? Well, bear with me, this is what you are going to see, right now. How can we see if predictive branching has an impact on the performance of an equals() method.
- 15:00 - 15:30 Well, there is only one way to find out, create the right set of data, and run all the equals() methods we have on it. All the use cases we are going to see are the same: we have a list of 400 Point instances. Point being the record we created earlier. We just loop over this list, and check if a given object is equal to the objects of this list.
- 15:30 - 16:00 This is the code we are running, on different sets of data. Bytheway this blackhole object is given to you by JMH, and you can use it to make sure that all the steps of your computation are actually executed, that HotSpot will not "optimize" things and remove some of your code because it finds that it's actually useless, which is always the case when you are running benchmarks. You absolutely want to avoid that situation,
- 16:00 - 16:30 this blackhole trick is very useful for that. The first use case runs on a list of points that are all the same instance of the Point record. So yes, they are all the same object. I'm not sure that you will see that a lot in applications, but... we need to start somewhere. And you can see that the result is what we expected: all the methods that first checked
- 16:30 - 17:00 for the instance equality perform better, and the pattern matching one is lagging behind. So in that case, performing this instance check is a good idea: you will get better performance. Let us move on to our next use case: we now have a list of objects, still 400, and this time they are all different instances, but they are all carrying the same state. So they are all equal.
- 17:00 - 17:30 And the object used to for the comparison is also equal to all of them, but a different instance. All the calls to this == are now false, the class comparisons are true, so all the methods are executing the code that is comparing the internal state of the objects. And because the state is the same, they have to execute all the instructions of this state comparison.
- 17:30 - 18:00 And this time the result is different. The Commons Lang pattern is lagging behind, as expected, obviously the goal of the design proposed by Commons Lang is not performance but readability, which is perfectly OK. All the others are doing roughly the same. But what's interesting is two things. First, using a simple pattern matching, without this instance check before, actually performs better than the rest, by about 40%, which is significant.
- 18:00 - 18:30 Even if you compare the plain instanceof with pattern matching, you can see that it is slower by about 10%, which is also significant. So pattern matching is actually doing very well in that situation. And because neither instanceof nor the pattern matching solutions are executing the instance check, they save on this test, which is executed by all the others. So in the case of a list of objects that are
- 18:30 - 19:00 different but that carry the same state, calling this instance check does not optimize anything, it is actually a performance hit. For the next use case we are going to use a list of objects of the same type, once again, but this time they are all different, and the object we compare is also different. We expect a result that is similar to the
- 19:00 - 19:30 previous one: the instance check is always false, the class check is always true, so the optimization there is in fact an overhead. And indeed, the results are similar. There is one result that is interesting in this table, which the instanceof versus the pattern matching one. Instanceof is better, with 142ns, and Pattern Matching is behind with 178ns.
- 19:30 - 20:00 You may be wondering why the result is different. Well, we are using a record pattern in this bench, that creates the two pattern variables x and y, by calling the accessors of this record. These accessors need to be called to create the pattern variables, because there may be some code in it that needs to be executed. Like a defensive copy for instance. So not calling the accessors would actually be a backdoor to access
- 20:00 - 20:30 the internal state carried by your records, without any security checks or validations. Then we use x for the comparison, that is failing, so y is not used. When you are just checking for the type with instanceof, no pattern variable is created. You create them lazily. In that case, you create x, and not y.
- 20:30 - 21:00 So in the case where you have accessors that are costly to execute, using a plain type pattern instead of a record pattern may actually be more efficient. For the last use case of this first series, we are going to use a list of other objects, of a different type. So the instance check is always false, the class check is also always false, and the rest of the code is not executed. So, on the one hand,
- 21:00 - 21:30 we are executing this code, and on the other hand, the instanceof is also always false, and the pattern variables are not created. So which one will perform better? Well, the benches give us a clear win for pattern matching, with a factor of almost 2: 180ns for the instance check solutions, and 95ns for the pattern matching and instanceof
- 21:30 - 22:00 ones. Checking for the instance equality is always false, so it's an overhead, adding the check for the class equality makes it more expensive than calling instanceof or using pattern matching. And as we can expect, adding this instance check before executing an instanceof or a pattern matching, actually degrades your performance, instead of improving them.
- 22:00 - 22:30 At this point, you can see that there is only one use case where conducting an instance check is an optimization, which is, you are comparing objects that are all the same instance. I'm not sure that this is the most common use case you'll come across in your applications. But so far, we have'nt seen any perdictive branching hit, because all the tests had the same results. Now, what we are going to do, it take these data sets again, and add some glitches in them:
- 22:30 - 23:00 objects that are different, to see the results. So let us take a look at the variations. We are going to check more use cases, still with a list of 400 elements, but we are going to add different objects in it, glitches, to trigger failures in the predictive branching algorithm. What is a glitch? Well, in a list where all the elements are the same as the one you compare
- 23:00 - 23:30 them to, a glitch is a different object. In a list where all the elements are different, a glitch is an element that is the same. Is it the same instance, or does it just carry the same state? Well, we need to check both, and this is what we are going to do. How are we going to distribute these glitches in a list? We are going to distribute them evenly, in the middle of the list. No glitch
- 23:30 - 24:00 at the beginning, nor at the end. So one glitch makes it in the middle, two glitches are a one third and two third, and 10 glitches, well, it looks like that. Now the use cases we are going to consider are the following. First use case: a list with objects that are all the same instance, and the glitch is a different object of the same type. Second use case: a list with objects that
- 24:00 - 24:30 are all different instances, but carrying the same state, so they are equal. And the glitch is also an object that is different, and of the same type. Third case, we are going to put different objects in your list, of the same type. And we are going to add two different kind of glitches. First, an object that is the same instance, and second, an object that is equal, but a different instance. And the last case is the same, but with a list
- 24:30 - 25:00 that contains objects of different types. That makes four use cases to look at, with two variations for two of them. When you have a list with instances that are all the same, adding glitches just makes the situation even worse for pattern matching. Checking for the instance gives you the result immediately. And even if the glitches degrade the performance,
- 25:00 - 25:30 it still much better than not doing it. So that situation is a clear win for all the solutions that are doing the instance check. Now the real question is: do you see this use case that often in your applications? Well, I'm not so sure. In the next use case, the instances are all different, but equal. And the glitch is a different object, of the same type. Pattern matching was performing better with
- 25:30 - 26:00 no glitch, and it is still the case, even if processing this data takes a little longer. This use case is a win for pattern matching and plain instanceof. What I find interesting in this bench, is that you can very clearly see that adding an instance check on the pattern matching or the plain instanceof solutions actually degrades your performance. You may be thinking that this simple instance check is almost free. Well, clearly it's not.
- 26:00 - 26:30 In the two next series, we have a list with objects of the same type, but different. And in this one the glitch is an object that is the always the same instance, and the same instance as the object we check it against. That should favor the solutions that are doing the instance check, right? Well, it does not. And the reason if that predictive branching is messing up
- 26:30 - 27:00 with this. On the one hand, you have an instance check that is mostly failing, and sometimes it's a success. And you saw on the predictive branching examples, that it is a situation that you do not want to find yourself into. And on the other hand, you have a pattern matching or an instanceof, with a type check somewhere, that is never failing. A situation that is much better. So in this series, where you may be
- 27:00 - 27:30 thinking that doing the instance check would be an optimization, it is actually something that, once again degrades your performance. And if you check the pattern matching and the instanceof lines, and compare them with the same solution with an instance check, you can see that doing this instance check is again a performance hit, by a factor of about 50%.
- 27:30 - 28:00 The following series is the same as the previous one, the only difference is that the glitch is a different instance that carries the same state. That is an object that is equal. The most interesting thing is that if you compare these numbers with the previous series, you can see that the instance check solutions are actually performing better. Which may seems unexpected, because this instance check is always failing in this series. Well, it's better, because predictive
- 28:00 - 28:30 branching is playing with you in this case, and it was playing against you in the previous one. The performance of pattern matching and plain instanceof is the same as the previous series, which is also expected, and much better than all the other solutions, by more that 50%. And the degradation due to the instance check is also roughly the same on the pattern matching and the plain instanceof solutions. The last two series are about comparing
- 28:30 - 29:00 objects of different types, with a glitch that is the same object, and then a different object that is equal. So for this first case, the instance check is mostly failing, and sometimes a success, which is not good. Pattern matching is mostly performing better, and adding an instance check is always a degradation of your performance. When the amount of glitches becomes higher, some solutions are doing better than pattern matching, that is slowly degrading over
- 29:00 - 29:30 time. But, actually up to a certain point. Be careful, because if you continue to increase the amount of glitches, pattern matching and plain instanceof are actually performing better again. So just be aware of that, and carefully bench your application with the actual data your are working on. And the last series is the
- 29:30 - 30:00 same as the previous one, but instead of having an identical instance, the instances are different, still carrying the same state. It does not make much difference, the results are roughly the same as the previous series. The biggest difference is actually the degradation of the record case. And, once again, you can see that adding the instance check degrades the performance for both pattern matching and instanceof, that becomes better again as the number of glitches increases.
- 30:00 - 30:30 So, what can you make of all this? How all this can help you write a better code? If there is one conclusion you should draw, it is the following: measure, don't guess. You saw that there are many cases where checking for the instance of an object actually degrades your performances. It's actually what is happenning in most of the cases I showed you.
- 30:30 - 31:00 But as usual, choosing the right solution is really about understanding what are your most common use cases. What kind of objects are you comparing? Are they all of the same type? Are they all different, or sometimes you are comparing identical instances? What it is the amount of failing tests you are expecting? Are they mostly failing, mostly successful, or evenly balanced?
- 31:00 - 31:30 In short: what is your application doing, how is it working? What kind of data are you processing? As a rule of thumb, in most of the cases I just showed you, doing this instance check is actually a performance hit. The only use case we saw where it is performing better is when all your objects are the same instance. In all the others,
- 31:30 - 32:00 not doing it is probably your best bet. Should you rely on the code your IDE can generate for you? Well, to be honest I have mixed feelings about it. These patterns haven't changed much in years. Like 15 years, maybe even more. And, what was true perfomance wise 15 or 20 years ago might have changed now.
- 32:00 - 32:30 Maybe thinking about these patterns of code again could be a good idea. But if you are lucky enough to have an application running on Java 21, pattern matching is definitely your best choice. If you have complex accessors, type pattern is probably your best choice, and if not, record pattern will give you a simpler solution, with a more readable code, and very good performance wise.
- 32:30 - 33:00 And with this I'm out of coffee, so, that's it for today, talk to you soon!