Search code examples
rustinterfacetraits

Is Rust trait the same as Java interface


It looks to me that Rust's trait is the same thing as Java's interface - a set of functions that needs to be implemented on object.

Is there a technical reason for naming it trait not interface or is it just some preference?


Solution

  • Rust traits and Java interfaces both address the problem of having multiple possible implementations that adhere to some convention/protocol/interface for interacting with a value/object, without constraining the implementation details as a Java superclass does. They can be used in many of the same situations. However, they are different in many details, mostly meaning that Rust traits are more powerful:

    • Java interfaces require that the implementing object have methods with specific names. Each Rust trait has a completely separate namespace. In Java, two interfaces might be impossible to implement together:

      interface Foo {
        void someMethod();
      }
      interface Bar {
        int someMethod();
      }
      class TwoInterfaces implements Foo, Bar {
        public int someMethod();  // The return type must be void and also must be int
      }
      

      In Rust, when you implement a trait for a type, you do so with a separate impl block specifying that trait, so it is explicit which trait each method/function belongs to. This means that implementing a trait for a type cannot conflict with a different trait (except at call sites that have both traits in scope, which must disambiguate by using function syntax instead of method syntax).

    • Java traits can have generics (type parameters), SomeInterface<T>, but an object can implement an interface only once.

      class TwoGenerics implements Foo<Integer>, Foo<String> {
        public void someMethod(??? value) {}  // Can't implement, must be one method
      }
      interface Foo<T> {
        void someMethod(T value);
      }
      

      In Rust, a trait may be implemented for many types, and these are basically considered just like different traits:

      struct TwoGenerics;
      
      trait Foo<T> {
          fn some_method(&self, input: T);
      }
      
      impl Foo<i32> for TwoGenerics {
          fn some_method(&self, input: i32) {}
      }
      impl Foo<String> for TwoGenerics {
          fn some_method(&self, input: String) {}
      }
      

      To get Java-like behavior of requiring there to be one particular type for any implementing type, you can define an associated type instead of a generic:

      struct Thing;
      
      trait Foo {
          type Input;
          fn some_method(&self, input: Self::Input);
      }
      
      impl Foo for Thing {
          type Input = i32;
          fn some_method(&self, input: i32) {}
      }
      
    • In Rust, a trait may be implemented for a type you didn't define, if you defined the trait. Thus, you can define a trait in your crate and then implement it for relevant types in the standard library (or other libraries you depend on), such as for serialization, random generation, traversal, etc. In Java, workarounds such as run-time type checks are required to achieve similar results.

    • In Rust, a trait may have a generic implementation that applies to all types meeting some criteria (bounds), or an implementation that applies to a particular generic type only when its parameters are suitable. For example, in the standard library, there is (approximately)

      impl<T> Clone for Vec<T> where T: Clone {...}
      

      so a Vec is clonable if and only if its contents are. In Java, a class cannot conditionally implement an interface, which is problematic for any recursive property: for example, list instanceof Serializable might be true while the list will fail to serialize because one or more of its elements is not serializable.

    • Rust traits may have associated constants, types, and non-method functions (analogous to Java static methods), all of which may be different for each implementing type. When Java interfaces have static members, they have only one implementation for the entire interface.

      For example, the Default trait in Rust allows constructing a new instance of any implementing type by calling Default::default() or T::default(). In Java, you need to create an interface implemented by separate “factory” classes to do this (but then the factories can have their own state/data, so you might still choose to use a factory trait and types in Rust).

    • Rust traits can have functions which accept multiple inputs (or produce outputs) that are also of the implementing type. Java interfaces cannot refer to the implementing type; they can only add a type parameter to play this role, which the language doesn't require to be the same. (On the other hand, Java has subclassing (subtyping) where Rust doesn't, and so the situation is necessarily more complicated when you have a collection of instances of different concrete type but the same supertype.)

    There's probably many more details that could be mentioned, but I think these cover a lot of the ways in which you can or must use them differently even though they are meant for the same tasks.


    As to the name “trait” versus “interface”, this is due to an existing concept of traits in computer science which are considered to have several specific properties, mostly that there is no inheritance and no overriding involved in implementing and using traits. This is closely related to the “separate namespace” I mentioned above.