开发者

Why interfaces must be declared in Java?

开发者 https://www.devze.com 2023-02-25 21:37 出处:网络
Sometimes we have several classes that have some methods with the same signature, but that don\'t correspond to a declared Java interface. For example, both JTextField and JButton (among several other

Sometimes we have several classes that have some methods with the same signature, but that don't correspond to a declared Java interface. For example, both JTextField and JButton (among several others in javax.swing.*) have a method

public void addActionListener(ActionListener l)

Now, suppose I wish to do something with objects that have that method; then, I'd like to have an interface (or perhaps to define it myself), e.g.

  public interface CanAddActionListener {
      public void addActionListener(ActionListener l);
  }

so that I could write:

  public void myMethod(CanAddActionListener aaa, ActionListener li) {
         aaa.addActionListener(li);
         ....

But, sadly, I can't:

     JButton button;
     ActionListener li;
     ...
     this.myMethod((CanAddActionListener)button,li);

This cast would be illegal. The compiler knows that JButton is not a CanAddActionListener,开发者_运维百科 because the class has not declared to implement that interface ... however it "actually" implements it.

This is sometimes an inconvenience - and Java itself has modified several core classes to implement a new interface made of old methods (String implements CharSequence, for example).

My question is: why this is so? I understand the utility of declaring that a class implements an interface. But anyway, looking at my example, why can't the compiler deduce that the class JButton "satisfies" the interface declaration (looking inside it) and accept the cast? Is it an issue of compiler efficiency or there are more fundamental problems?

My summary of the answers: This is a case in which Java could have made allowance for some "structural typing" (sort of a duck typing - but checked at compile time). It didn't. Apart from some (unclear for me) performance and implementations difficulties, there is a much more fundamental concept here: In Java, the declaration of an interface (and in general, of everything) is not meant to be merely structural (to have methods with these signatures) but semantical: the methods are supposed to implement some specific behavior/intent. So, a class which structurally satisfies some interface (i.e., it has the methods with the required signatures) does not necessarily satisfies it semantically (an extreme example: recall the "marker interfaces", which do not even have methods!). Hence, Java can assert that a class implements an interface because (and only because) this has been explicitly declared. Other languages (Go, Scala) have other philosophies.


Java's design choice to make implementing classes expressly declare the interface they implement is just that -- a design choice. To be sure, the JVM has been optimized for this choice and implementing another choice (say, Scala's structural typing) may now come at additional cost unless and until some new JVM instructions are added.

So what exactly is the design choice about? It all comes down to the semantics of methods. Consider: are the following methods semantically the same?

  • draw(String graphicalShapeName)
  • draw(String handgunName)
  • draw(String playingCardName)

All three methods have the signature draw(String). A human might infer that they have different semantics from the parameter names, or by reading some documentation. Is there any way for the machine to tell that they are different?

Java's design choice is to demand that the developer of a class explicitly state that a method conforms to the semantics of a pre-defined interface:

interface GraphicalDisplay {
    ...
    void draw(String graphicalShapeName);
    ...
}

class JavascriptCanvas implements GraphicalDisplay {
    ...
    public void draw(String shape);
    ...
}

There is no doubt that the draw method in JavascriptCanvas is intended to match the draw method for a graphical display. If one attempted to pass an object that was going to pull out a handgun, the machine can detect the error.

Go's design choice is more liberal and allows interfaces to be defined after the fact. A concrete class need not declare what interfaces it implements. Rather, the designer of a new card game component may declare that an object that supplies playing cards must have a method that matches the signature draw(String). This has the advantage that any existing class with that method can be used without having to change its source code, but the disadvantage that the class might pull out a handgun instead of a playing card.

The design choice of duck-typing languages is to dispense with formal interfaces altogether and simply match on method signatures. Any concept of interface (or "protocol") is purely idiomatic, with no direct language support.

These are but three of many possible design choices. The three can be glibly summarized like this:

Java: the programmer must explicitly declare his intent, and the machine will check it. The assumption is that the programmer is likely to make a semantic mistake (graphics / handgun / card).

Go: the programmer must declare at least part of his intent, but the machine has less to go on when checking it. The assumption is that the programmer is likely to might make a clerical mistake (integer / string), but not likely to make a semantic mistake (graphics / handgun / card).

Duck-typing: the programmer needn't express any intent, and there is nothing for the machine to check. The assumption is that programmer is unlikely to make either a clerical or semantic mistake.

It is beyond the scope of this answer to address whether interfaces, and typing in general, are adequate to test for clerical and semantic mistakes. A full discussion would have to consider build-time compiler technology, automated testing methodology, run-time/hot-spot compilation and a host of other issues.

It is acknowledged that the draw(String) example are deliberately exaggerated to make a point. Real examples would involve richer types that would give more clues to disambiguate the methods.


Why can't the compiler deduce that the class JButton "satisfies" the interface declaration (looking inside it) and accept the cast? Is it an issue of compiler efficiency or there are more fundamental problems?

It is a more fundamental issue.

The point of an interface is to specify that there is a common API / set of behaviors that a number of classes support. So, when a class is declared as implements SomeInterface, any methods in the class whose signatures match method signatures in the interface are assumed to be methods that provide that behavior.

By contrast, if the language simply matched methods based on signatures ... irrespective of the interfaces ... then we'd be liable to get false matches, when two methods with the same signature actually mean / do something semantically unrelated.

(The name for the latter approach is "duck typing" ... and Java doesn't support it.)


The Wikipedia page on type systems says that duck typing is neither "nominative typing" or "structural typing". By contrast, Pierce doesn't even mention "duck typing", but he defines nominative (or "nominal" as he calls it) typing and structural typing as follows:

"Type systems like Java's, in which names [of types] are significant and subtyping is explicitly declared, are called nominal. Type systems like most of the ones in this book in which names are inessential and subtyping is defined directly on the structure of the types, are called structural."

So by Pierce's definition, duck typing is a form of structural typing, albeit one that is typically implemented using runtime checks. (Pierce's definitions are independent of compile-time versus runtime-checking.)

Reference:

  • "Types and Programming Languages" - Benjamin C Pierce, MIT Press, 2002, ISBN 0-26216209-1.


Likely it's a performance feature.

Since Java is statically typed, the compiler can assert the conformance of a class to an identified interface. Once validated, that assertion can be represented in the compiled class as simply a reference to the conforming interface definition.

Later, at runtime, when an object has its Class cast to the interface type, all the runtime needs to do is check the meta data of the class to see if the class that it is being cast too is compatible (via the interface or the inheritance hierarchy).

This is a reasonably cheap check to perform since the compiler has done most of the work.

Mind, it's not authoritative. A class can SAY that it conforms to an interface, but that doesn't mean that the actual method send about to be executed will actually work. The conforming class may well be out of date and the method may simply not exist.

But a key component to the performance of java is that while it still must actually do a form of dynamic method dispatch at runtime, there's a contract that the method isn't going to suddenly vanish behind the runtimes back. So once the method is located, its location can be cached in the future. In contrast to a dynamic language where methods may come and go, and they must continue to try and hunt the methods down each time one is invoked. Obviously, dynamic languages have mechanisms to make this perform well.

Now, if the runtime were to ascertain that an object complies with an interface by doing all of the work itself, you can see how much more expensive that can be, especially with a large interface. A JDBC ResultSet, for example, has over 140 methods and such in it.

Duck typing is effectively dynamic interface matching. Check what methods are called on an object, and map it at runtime.

All of that kind of information can be cached, and built at runtime, etc. All of this can (and is in other languages), but having much of this done at compile time is actually quite efficient both on the runtimes CPU and its memory. While we use Java with multi GB heaps for long running servers, it's actually pretty suitable for small deployments and lean runtimes. Even outside of J2ME. So, there is still motivation to try and keep the runtime footprint as lean as possible.


Duck typing can be dangerous for the reasons Stephen C discussed, but it is not necessarily the evil that breaks all static typing. A static and more safe version of duck typing lies at the heart of Go's type system, and a version is available in Scala where it is called "structural typing." These versions still perform a compile time check to make sure the object obeys the requirements, but have potential problems because they break the design paradigm that has implementing an interface always an intentional decision.

See http://markthomas.info/blog/?p=66 and http://programming-scala.labs.oreilly.com/ch12.html and http://beust.com/weblog/2008/02/11/structural-typing-vs-duck-typing/ for a discusion of the Scala feature.


I can't say I know why certain design decisions were made by the Java development team. I also caveat my answer with the fact that those individuals are far smarter than I'll ever be with regards to software development and (particularly) language design. But, here's a crack at trying to answer your question.

In order to understand why they may not have chosen to use an interface like "CanAddActionListener" you have to look at the advantages of NOT using an interface and, instead, preferring abstract (and, ultimately, concrete) classes.

As you may know, when declaring abstract functionality, you can provide default functionality to subclasses. Okay...so what? Big deal, right? Well, particularly in the case of designing a language, it is a big deal. When designing a language, you will need to maintain those base classes over the life of the language (and you can be sure that there will be changes as your language evolves). If you had chosen to use interfaces, instead of providing base functionality in an abstract class, any class that implements the interface will break. This is particularly important after publication - once customers (developers in this case) start using your libraries, you can't change up the interfaces on a whim or you are going to have a lot of pissed of developers!

So, my guess is that the Java development team fully realized that many of their AbstractJ* classes shared the same method names, it would not be advantageous in having them share a common interface as it would make their API rigid and inflexible.

To sum it up (thank you to this site here):

  • Abstract classes can easily be extended by adding new (non-abstract) methods.
  • An interface cannot be modified without breaking its contract with the classes that implement it. Once an interface has been shipped, its member set is permanently fixed. An API based on interfaces can only be extended by adding new interfaces.

Of course, this is not to say that you could do something like this in your own code, (extend AbstractJButton and implement CanAddActionListener interface) but be aware of the pitfalls in doing so.


Interfaces represent a form of substitution class. A reference of type which implements or inherits from a particular interface may be passed to a method which expects that interface type. An interface will generally not only specify that all implementing classes must have methods with certain names and signatures, but it will generally also have an associated contract which specifies that all legitimate implementing classes must have methods with certain names and signatures, which behave in certain designated ways. It is entirely possible that even if two interfaces contain members with the same names and signatures, an implementation may satisfy the contract of one but not the other.

As a simple example, if one were designing a framework from scratch, one might start out with an Enumerable<T> interface (which can be used as often as desired to create an enumerator which will output a sequence of T's, but different requests may yield different sequences), but then derive from it an interface ImmutableEnumerable<T> which would behave as above but guarantee that every request would return the same sequence. A mutable collection type would support all of the members required for ImmutableEnumerable<T>, but since requests for enumeration received after a mutation would report a different sequence from requests made before, it would not abide by the ImmutableEnumerable contract.

The ability of an interface to be regarded as encapsulating a contract beyond the signatures of its members is one of the things that makes interface-based programming more semantically powerful than simple duck-typing.

0

精彩评论

暂无评论...
验证码 换一张
取 消

关注公众号