We usually write most of our code using only a few features of the Java language, and each Stream we instantiate and each @AutoWired annotation that prefixes the instance is sufficient to accomplish most of our goals. Sometimes, however, we must turn to a part of the language that is rarely used: a hidden part of the language that is often used for a specific purpose.

In this article, we’ll explore four techniques to help improve development simplicity and readability. Not all of these techniques are appropriate for every situation, or for most of them. For example, there may be only a few centralized methods suitable for covariant return types or only a few generic classes suitable for using the pattern of cross-generic types, while other methods may improve the readability and clarity of most code base intents. In any case, it is important not only to know these techniques, but also to know when to apply them.

1. Covariant return types

Even the most fascinating Java manuals contain descriptions of inheritance, interfaces, abstract classes, and method coverage, but rarely explore the more complex possibilities of covering methods. For example, the following code snippet will come as no surprise to even new Java developers:

public interface Animal {

public String makeNoise();

}

public class Dog implements Animal {

@Override

public String makeNoise() {

return “Woof”;

}

}

public class Cat implements Animal {

@Override

public String makeNoise() {

return “Meow”;

}

}

This is the basic concept of polymorphism: an object’s methods can be called according to its interface (Animal::makeNoise) but the actual behavior of that method call depends on the implementation type (Dog::makeNoise). For example, the output of the following method will change depending on whether a Dog or Cat object is passed to the method:

public class Talker {

public static void talk(Animal animal) {

System.out.println(animal.makeNoise());

}

}

Talker.talk(new Dog()); // Output: Woof

Talker.talk(new Cat()); // Output: Meow

While this is a common technique in many Java applications, there is another action you might take when overriding a method: change the return type. Although this may be an unlimited way to override methods, there are strict restrictions on the return types of override methods. According to the Java 8 SE language specification (page 248) :

If one method declaration d 1 (containing return type R 1) overwrites or hides the declaration of another method D 2 (containing return type R 2), then D 1 must be a return type substitute, or a compile-time error will occur.

Where return-type-substitutable (ibid., p. 240) was defined as:

  1. If R1 is invalid, R2 is invalid

  2. If R1 is primitive, R2 is the same as R1

  3. If R1 is a reference type, one of the following conditions is met:

A. R1 applies to parameters of type D2, which is a subtype of R2

B. R1 can be converted to a subtype of R2 by an unchecked conversion

C. do not have the same signature as the d2, d1 and R1 = | | R2

Arguably, the most interesting examples are Rules 3.a. And 3.b. : When overriding a method, subtypes of return types can be declared as override return types, for example:

  1. public interface CustomCloneable {

  2. public Object customClone();

  3. }

  4. public class Vehicle implements CustomCloneable {

  5. private final String model;

  6. public Vehicle(String model) {

  7. this.model = model;

  8. }

  9. @Override

  10. public Vehicle customClone() {

  11. return new Vehicle(this.model);

  12. }

  13. public String getModel() {

  14. return this.model;

  15. }

  16. }

  17. Vehicle originalVehicle = new Vehicle(“Corvette”);

  18. Vehicle clonedVehicle = originalVehicle.customClone();

  19. System.out.println(clonedVehicle.getModel());

Although the original return type of Clone () is Object, we can call getModel() in clonedVehicle (without explicit conversion) because we have rewritten the return type of Vehicle :: Clone to Vehicle. This eliminates the need for garble, and we know that the return type we are looking for is Vehicle, even though it is declared Object (equivalent to secure delivery based on prior information, but not strictly safe) :

  1. Vehicle clonedVehicle = (Vehicle) originalVehicle.customClone();

Note that we can still declare the Vehicle type as Object, and the return type will revert to the original type of Object:

  1. Object clonedVehicle = originalVehicle.customClone();

  2. System.out.println(clonedVehicle.getModel()); // ERROR: getModel not a method of Object

Note that return types cannot be overloaded for generic parameters, but they can be overloaded for generic classes. For example, if a base class or interface method returns a List, the return type of a subclass may override ArrayList, but may not override List.

2. Cross generic types

Creating generic classes is a good way to create a set of classes that interact with composite objects. For example, a List only stores and retrieves objects of type T without knowing the nature of the elements it contains, and in some cases we want to restrict our generic type parameter (T) to having specific characteristics. For example, the following interface:

  1. public interface Writer {

  2. public void write();

  3. }

We might want to create specific Writers groups, as shown in the Composite Patterns below:

  1. public class WriterComposite implements Writer {

  2. private final List writers;

  3. public WriterComposite(List writers) {

  4. this.writers = writer;

  5. }

  6. @Override

  7. public void write() {

  8. for (Writer writer: this.writers) {

  9. writer.write();

  10. }

  11. }

  12. }

We can now traverse the Writers tree and do not know whether the specific Writer we encounter is a standalongWriter or a combination of Writers. What if we also want our composition to be a combination of reader and writer? For example, if we have the following interface:

  1. public interface Reader {

  2. public void read();

  3. }

How do we change our WriterComposite to ReaderWriterComposite? There is a technique for creating a new interface, ReaderWriter, to fuse the Reader and Writer interfaces:

  1. public interface ReaderWriter implements Reader, Writer {}

We can then modify the existing WriterComposite to the following:

  1. public class ReaderWriterComposite implements ReaderWriter {

  2. private final List readerWriters;

  3. public WriterComposite(List readerWriters) {

  4. this.readerWriters = readerWriters;

  5. }

  6. @Override

  7. public void write() {

  8. for (Writer writer: this.readerWriters) {

  9. writer.write();

  10. }

  11. }

  12. @Override

  13. public void read() {

  14. for (Reader reader: this.readerWriters) {

  15. reader.read();

  16. }

  17. }

  18. }

While this accomplishes our goal, we create bloat in our code: we create an interface whose sole purpose is to merge two existing interfaces together. As more and more interfaces emerge, we will see an explosion of bloated combinations. For example, if we created a new Modifier interface, we would now need the createReaderModifier, WriterModifier, and ReaderWriter interfaces. Note that these interfaces do not add any functionality: they simply merge existing interfaces.

To eliminate this bloat, we need to be able to specify that our ReaderWriterComposite accepts generic type parameters only if they are both readers and writers. Crossing generic types allows us to do this. To specify generic type parameters, both reader and Writer interfaces must be deployed. We use the & operator between generic type constraints:

  1. public class ReaderWriterComposite implements Reader, Writer {

  2. private final List readerWriters;

  3. public WriterComposite(List readerWriters) {

  4. this.readerWriters = readerWriters;

  5. }

  6. @Override

  7. public void write() {

  8. for (Writer writer: this.readerWriters) {

  9. writer.write();

  10. }

  11. }

  12. @Override

  13. public void read() {

  14. for (Reader reader: this.readerWriters) {

  15. reader.read();

  16. }

  17. }

  18. }

Without the expanded inheritance tree, we can now constrain common type parameters to deploy multiple interfaces. Note that the same restriction can specify whether one of the interfaces is an abstract class or a concrete class. For example, if we change the Writer interface to an abstract class, something like this:

  1. public abstract class Writer {

  2. public abstract void write();

  3. }

We can still restrict our generic type arguments to Reader and Writer, but Writer (since it is an abstract class rather than an interface) must be specified first. (Also note that our ReaderWriterComposite now extends the Writer abstract class and deployable the Reader interface. Rather than implementing both.)

  1. public class ReaderWriterComposite extends Writer implements Reader {

  2. // Same class body as before

  3. }

Equally important, this cross generic type can be used with more than two interfaces (or one abstract class and multiple interfaces). For example, if we want our composition to also include Modifierinterface, we can write our class definition as follows:

  1. public class ReaderWriterComposite implements Reader, Writer, Modifier {

  2. private final List things;

  3. public ReaderWriterComposite(List things) {

  4. this.things = things;

  5. }

  6. @Override

  7. public void write() {

  8. for (Writer writer: this.things) {

  9. writer.write();

  10. }

  11. }

  12. @Override

  13. public void read() {

  14. for (Reader reader: this.things) {

  15. reader.read();

  16. }

  17. }

  18. @Override

  19. public void modify() {

  20. for (Modifier modifier: this.things) {

  21. modifier.modify();

  22. }

  23. }

  24. }

While the above can be done, this may be a sign of code smell (The Reader, Writer, and Modifier objects may be more specific, such as File)

For more information about crossed generic types, see the Java 8 language specification.

3. Automatically close classes

Creating a resource class is a common practice, but maintaining the integrity of that resource can be challenging, especially when exception handling is involved. For example, suppose we create a Resource class, Resource, and want to perform operations on that Resource, which might raise an exception (instantiation might also raise an exception) :

  1. public class Resource {

  2. public Resource() throws Exception {

  3. System.out.println(“Created resource”);

  4. }

  5. public void someAction() throws Exception {

  6. System.out.println(“Performed some action”);

  7. }

  8. public void close() {

  9. System.out.println(“Closed resource”);

  10. }

  11. }

In either case (raising an exception or not), we want to close our resources to ensure that no resources leak. The normal procedure is to close our close() method ina finally block, ensuring that no matter what happens, our resource is closed before the enclosing execution scope completes:

  1. Resource resource = null;

  2. try {

  3. resource = new Resource();

  4. resource.someAction();

  5. }

  6. catch (Exception e) {

  7. System.out.println(“Exception caught”);

  8. }

  9. finally {

  10. resource.close();

  11. }

Through a simple inspection, we found that a lot of boilerplate code was detracting from the readability of the execution of the Resource object someAction(). To remedy this, Java 7 introduced the try-with-Resources declaration, where resources can be created in the try declaration and closed automatically before leaving the scope of the try execution. In order for a class to use try-with-resources, the auto-close interface must be deployed:

  1. public class Resource implements AutoCloseable {

  2. public Resource() throws Exception {

  3. System.out.println(“Created resource”);

  4. }

  5. public void someAction() throws Exception {

  6. System.out.println(“Performed some action”);

  7. }

  8. @Override

  9. public void close() {

  10. System.out.println(“Closed resource”);

  11. }

  12. }

Our Resource class now has an autoclose interface, and we can clean up the code to ensure that the Resource is closed before it leaves the scope of the try execution.

  1. try (Resource resource = new Resource()) {

  2. resource.someAction();

  3. }

  4. catch (Exception e) {

  5. System.out.println(“Exception caught”);

  6. }

Compared to non-try-with-Resource techniques, this process is less messy and retains the same security (the resource remains closed until the try execution scope is complete). If we execute the try-with-resource above, we get the following output:

  1. Created resource

  2. Performed some action

  3. Closed resource

To demonstrate the security of this try-with-resource technique, we can change someAction() to throw an Exception:

  1. public class Resource implements AutoCloseable {

  2. public Resource() throws Exception {

  3. System.out.println(“Created resource”);

  4. }

  5. public void someAction() throws Exception {

  6. System.out.println(“Performed some action”);

  7. throw new Exception();

  8. }

  9. @Override

  10. public void close() {

  11. System.out.println(“Closed resource”);

  12. }

  13. }

If we re-run the try-with-resources declaration, we get the following output:

  1. Created resource

  2. Performed some action

  3. Closed resource

  4. Exception caught

Notice that even if an Exception is thrown when executing the someAction() method, our resource is still closed and Exception is caught. This ensures that our resources are guaranteed to be closed before leaving the try execution scope. It is also important to note that the Resource deployable interface Closeable can still use the try-with-resources declaration. The difference between deploying an automatically closed interface and a closeable interface is the type of Exception thrown from the close() method signature: Exceptionand IOException. In our example, we simply change the signature of the close() method without raising an exception.

4. Final classes and methods

In almost all cases, the classes we create can be extended by another developer and customized to their needs (we can extend our own classes), even if we don’t want to extend ours. While this is sufficient in most cases, sometimes we don’t want methods to be overridden, or our classes to be extended. For example, if we create the File class to encapsulate the reads and writes of files in the File system, we might not want any subclasses to override our read(Int Bytes) and Write (String Data) methods (which could corrupt the File system if the bare bodies of those methods were changed). In this case, we mark the non-extensible method as final:

  1. public class File {

  2. public final String read(int bytes) {

  3. // Execute the read on the file system

  4. return “Some read data”;

  5. }

  6. public final void write(String data) {

  7. // Execute the write to the file system

  8. }

  9. }

Now, if another class wishes to override a read or write method, a compilation error is raised: the final method cannot be overwritten from File. Not only do we record that our methods should not be overridden, but the compiler also ensures that this intent is not executed at compile time.

When extending this idea to an entire class, there may be times when we don’t want our class to be extended. This not only makes every method of the class unexecutable, but also makes it impossible to create subtypes of the class. For example, if we were creating a security framework to use a key generator, we probably wouldn’t want any external developers to extend our key generator and override the generation algorithm (custom features might affect the system) :

  1. public final class KeyGenerator {

  2. private final String seed;

  3. public KeyGenerator(String seed) {

  4. this.seed = seed;

  5. }

  6. public CryptographicKey generate() {

  7. / /… Do some cryptographic work to generate the key…

  8. }

  9. }

By making our KeyGenerator class the final class, the compiler ensures that no class extends our class and passes it to our framework as a valid cryptographic KeyGenerator. While simply marking the thegenerate() method as final may seem sufficient, this does not prevent developers from creating custom key generators and using them as valid generators. Because our system is security-oriented, we should trust the outside world as little as possible (a smart developer can change the generation algorithm by changing the functionality of other methods in the KeyGenerator class).

While this may seem like a public repudiation of the open/closed principle, there are good reasons for doing so. As you can see from our security example above, many times we can’t get external developers to do what they want with our application, and we have to make very careful decisions about integration. This concept is ubiquitous, for example C# defaults a class as final (it cannot be extended), and it must be specified as open by the developer. In addition, we should be very careful about which classes can be extended and which methods can be overridden.

conclusion

Although we wrote most of the code using only a small portion of Java’s capabilities, it was enough to solve most of the problems we encountered. Sometimes we need to dig deep into forgotten or unknown parts of the language to solve specific problems. Techniques such as covariant return types and cross-generic types can be used for one-off situations, while methods that automatically close resources and final methods and classes can be used to produce more readable and accurate code. You can combine these skills with everyday programming practices to help you write Better Java code. 1, want to learn JAVA this technology, interested in JAVA zero foundation, want to be engaged in JAVA work. 2. Those who have worked for 1-5 years and feel their skills are not good and want to improve their skills. 3. There is also want to exchange and study together. 5. No small plus group, thank you. Please forward this article with the original link, otherwise will be investigated legal responsibility