Traits vs interfaces
In one sentence
Section titled “In one sentence”A trait is a set of methods that types can implement, like a Java interface with default methods, but without inheritance between types.
The Java you know
Section titled “The Java you know”A Java interface declares methods. A class implements it, and since Java 8 the interface can have default methods. For shared fields, Java uses an abstract base class.
Java
interface PriceRule { String name();
long apply(long cents);
default String describe(long cents) { return name() + ": " + cents + " -> " + apply(cents); }}
record PercentOff(long percent) implements PriceRule { public String name() { return "percent off"; }
public long apply(long cents) { return cents - cents * percent / 100; }}Rust
pub trait PriceRule { fn name(&self) -> &str;
fn apply(&self, cents: u64) -> u64;
/// A default method, like a Java `default` method in an interface. fn describe(&self, cents: u64) -> String { format!("{}: {} -> {}", self.name(), cents, self.apply(cents)) }}
pub struct PercentOff(pub u64);
impl PriceRule for PercentOff { fn name(&self) -> &str { "percent off" }
fn apply(&self, cents: u64) -> u64 { cents - cents * self.0 / 100 }}
pub struct FreeShippingOver(pub u64);
impl PriceRule for FreeShippingOver { fn name(&self) -> &str { "free shipping" }
fn apply(&self, cents: u64) -> u64 { if cents >= self.0 { cents } else { cents + 495 } }}The Rust you will see
Section titled “The Rust you will see”The structure is the same as in Java: a trait declares methods, can give default bodies, and each type implements it in an impl Trait for Type block. The differences are in what else is possible, and in what is not:
- No inheritance of data. A trait has no fields. There are no abstract classes and no
extends. - Implement for any type. You can implement your trait for a type from another crate, even for
u64orVec<T>. - Static dispatch by default. A generic function that takes a trait is compiled once for each type. Dynamic dispatch, as in Java, is opt-in with
dyn. - Associated types and constants. A trait can declare a type (
type Item;) or a constant (const MAX: u32;) that each implementation chooses.
pub trait PriceRule { fn name(&self) -> &str;
fn apply(&self, cents: u64) -> u64;
/// A default method, like a Java `default` method in an interface. fn describe(&self, cents: u64) -> String { format!("{}: {} -> {}", self.name(), cents, self.apply(cents)) }}
pub struct PercentOff(pub u64);
impl PriceRule for PercentOff { fn name(&self) -> &str { "percent off" }
fn apply(&self, cents: u64) -> u64 { cents - cents * self.0 / 100 }}
pub struct FreeShippingOver(pub u64);
impl PriceRule for FreeShippingOver { fn name(&self) -> &str { "free shipping" }
fn apply(&self, cents: u64) -> u64 { if cents >= self.0 { cents } else { cents + 495 } }}- Line 1:
pub trait: the interface. Every method in it is public if the trait is public. - Line 7: A default method. It can call the required methods, like a Java
defaultmethod. - Line 12: A tuple struct with one field,
self.0. The struct does not say which traits it implements. - Line 14:
impl PriceRule for PercentOffis theimplementsclause, in its own block. It can be in another file of the crate. - Line 26: A second implementation.
describecomes from the default.
Implement a trait for a type you did not write
Section titled “Implement a trait for a type you did not write”You can add methods to u64 by defining a trait and implementing it. This is like C# extension methods. The caller must import the trait with use to call its methods:
/// A trait of this crate, implemented for `u64`, a type from the standard library.pub trait CentsExt { fn as_euros(&self) -> String;}
impl CentsExt for u64 { fn as_euros(&self) -> String { format!("€{}.{:02}", self / 100, self % 100) }}
pub fn label(price: u64) -> String { price.as_euros() // reads like a method of `u64`}The orphan rule limits this: either the trait or the type must be from your crate. You cannot implement Display (from the standard library) for Vec<u64> (from the standard library). This rule stops two crates from giving conflicting implementations.
How to read it
Section titled “How to read it”Static dispatch: generics and impl Trait
Section titled “Static dispatch: generics and impl Trait”A function that takes “any PriceRule” can be written in two ways that mean the same:
/// Generic: the compiler makes a copy of this function for each rule type.pub fn final_price<R: PriceRule>(rule: &R, cents: u64) -> u64 { rule.apply(cents)}
/// The same, written with `impl Trait` in argument position.pub fn final_price_short(rule: &impl PriceRule, cents: u64) -> u64 { rule.apply(cents)}The compiler makes a separate copy of the function for each concrete type that calls it (monomorphization, see Generics). Each call goes directly to the right apply, and the compiler can inline it. There is no vtable and no heap allocation.
Dynamic dispatch: dyn Trait
Section titled “Dynamic dispatch: dyn Trait”When one list must hold different types, you need a trait object: &dyn PriceRule or Box<dyn PriceRule>. This is how all Java interface calls work: a pointer to the value plus a pointer to a table of methods (the vtable).
/// Trait objects: different rule types in one list. Each call goes through a vtable.pub fn apply_all(rules: &[Box<dyn PriceRule>], cents: u64) -> u64 { rules.iter().fold(cents, |price, rule| rule.apply(price))}
pub fn checkout_rules() -> Vec<Box<dyn PriceRule>> { vec![Box::new(PercentOff(10)), Box::new(FreeShippingOver(5_000))]}| Rust | Dispatch | Java |
|---|---|---|
fn f<T: Rule>(r: &T) or fn f(r: &impl Rule) |
static, one copy per type | (no equivalent) |
fn f(r: &dyn Rule) |
dynamic, through a vtable | every interface call |
Vec<Box<dyn Rule>> |
dynamic, each value on the heap | List<Rule> |
enum Rule { A, B } with match |
static, closed set of types | sealed interface with switch |
When you review, ask which one the code needs. A fixed, known set of types is often an enum (see Enums and pattern matching). One type per call is a generic. Only an open set of types in one collection needs dyn. See S22: Box<dyn Trait> where a generic or enum is sufficient.
Standard traits you will see
Section titled “Standard traits you will see”Many “magic” methods in Java are traits in Rust. These are the ones you will meet in almost every codebase:
| Trait | What it gives | Java |
|---|---|---|
Debug |
{:?} formatting |
toString() for developers |
Display |
{} formatting and to_string() |
toString() for users |
Clone, Copy |
copies (see Clone and Copy) | copy constructor |
PartialEq, Eq, Hash |
==, map keys |
equals, hashCode |
PartialOrd, Ord |
<, sorting |
Comparable |
Default |
Type::default() |
no-argument constructor |
From, Into |
conversions that cannot fail | constructors, valueOf |
TryFrom, TryInto |
conversions that can fail | valueOf that throws |
AsRef, Borrow |
“can be viewed as a &T” |
(none) |
Deref |
*x and auto-deref, for smart pointers |
(none) |
Iterator, IntoIterator |
for loops, adapters |
Iterator, Iterable |
Drop |
cleanup at end of scope | AutoCloseable.close() |
Send, Sync |
safe to move or share between threads | (none) |
Error |
an error type with an optional cause | Throwable |
Two of them, written by hand:
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]pub struct Money { cents: u64,}
impl fmt::Display for Money { // `toString()` for users: used by `format!("{}")` and `to_string()` fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "€{}.{:02}", self.cents / 100, self.cents % 100) }}
impl From<u64> for Money { // a conversion that cannot fail: `Money::from(1999)` or `1999.into()` fn from(cents: u64) -> Self { Self { cents } }}impl From<u64> for Money also gives you Into<Money> for u64 for free, so 1999.into() works where a Money is expected.
Composition instead of inheritance
Section titled “Composition instead of inheritance”A Java base class gives shared fields and shared behavior. In Rust, shared fields go in a struct that each type has as a field. Shared behavior goes in a trait:
Java
abstract static class AuditedEntity { protected final String createdBy;
AuditedEntity(String createdBy) { this.createdBy = createdBy; }
String createdBy() { return createdBy; }}
static final class Invoice extends AuditedEntity { final String number;
Invoice(String createdBy, String number) { super(createdBy); this.number = number; }}Rust
pub struct AuditInfo { pub created_by: String, pub version: u32,}
/// Java: `class Invoice extends AuditedEntity`. Rust: `Invoice` has an `AuditInfo`.pub struct Invoice { pub audit: AuditInfo, pub number: String,}
pub struct Refund { pub audit: AuditInfo, pub invoice_number: String,}
/// Shared behavior goes in a trait that both types implement.pub trait Audited { fn audit(&self) -> &AuditInfo;
fn created_by(&self) -> &str { &self.audit().created_by }}
impl Audited for Invoice { fn audit(&self) -> &AuditInfo { &self.audit }}
impl Audited for Refund { fn audit(&self) -> &AuditInfo { &self.audit }}The trait has one required method (audit) that gives access to the shared data, and default methods built on it. This is a common pattern. It is more explicit than inheritance: each type says what it shares.
What good looks like
Section titled “What good looks like”- Small traits. One to five methods, for one capability:
Display,PriceRule,Payable. - A trait has a reason to exist: two or more implementations, a test double, or an extension point for other crates.
- Generics or
impl Traitby default,dyn Traitwhere a collection holds different types, andenumwhere the set of types is closed. - Standard traits instead of custom methods.
Displayinstead offn to_text(),Frominstead offn from_u64(),Defaultinstead offn empty(). - Derive what you can.
#[derive(Debug, Clone, PartialEq)]on most data types.
Red flags in AI output
Section titled “Red flags in AI output”- Deep trait hierarchies that copy a Java class hierarchy:
trait Document: AuditedEntity: Entity. See S13: Trait hierarchy that copies a Java class hierarchy. Derefto a “base” struct, to get inherited-looking methods. See S14:Derefused to imitate inheritance.Box<dyn Trait>everywhere, also where a generic or an enum works. See S22:Box<dyn Trait>where a generic or enum is sufficient.- A trait with exactly one implementation, named like a Java service (
OrderServiceandOrderServiceImpl), with no tests that use another implementation. as_any()and downcasting to imitateinstanceof. If the code must know the concrete type, the trait is the wrong abstraction. An enum is often the fix.- Custom
to_string(),equals()orclone_order()methods instead of the standard traits.
- S13Severity: MediumTrait hierarchy that copies a Java class hierarchyDeep supertrait chains that imitate inheritance.
- S14Severity: Medium
Derefused to imitate inheritanceA wrapper type that auto-dereferences to a "base class". - S22Severity: Low
Box<dyn Trait>where a generic or enum is sufficientDynamic dispatch and a heap allocation where the set of types is known.
Better vs worse
Section titled “Better vs worse”The task: an order store that a report function reads.
WorseAn
OrderServicetrait with oneOrderServiceImpl, and a&dynparameter. A Java interface-per-class habit.pub trait OrderService {fn find(&self, id: u64) -> Option<&Order>;fn total_cents(&self) -> u64;}pub struct OrderServiceImpl {orders: Vec<Order>,}impl OrderServiceImpl {pub fn new(orders: Vec<Order>) -> Self {Self { orders }}}impl OrderService for OrderServiceImpl {fn find(&self, id: u64) -> Option<&Order> {self.orders.iter().find(|o| o.id == id)}fn total_cents(&self) -> u64 {self.orders.iter().map(|o| o.total_cents).sum()}}pub fn report(service: &dyn OrderService) -> u64 {service.total_cents()}Run in Playground (opens the Rust Playground in a new tab) BetterA plain struct with methods. Add a trait when a second implementation exists.
pub struct Orders {orders: Vec<Order>,}impl Orders {pub fn new(orders: Vec<Order>) -> Self {Self { orders }}pub fn find(&self, id: u64) -> Option<&Order> {self.orders.iter().find(|o| o.id == id)}pub fn total_cents(&self) -> u64 {self.orders.iter().map(|o| o.total_cents).sum()}}pub fn report(orders: &Orders) -> u64 {orders.total_cents()}Run in Playground (opens the Rust Playground in a new tab)
The “worse” version compiles and works. Its costs: twice the code, a dynamic call for no reason, and a reader who looks for the second implementation. In Java, the interface also exists for mocking frameworks and dependency injection containers. In Rust, tests usually build real values, and a trait can be added later without changing callers much.
The trait is correct when there are real alternatives: a database store and an in-memory store, or a test double that tests really use. Then a generic parameter (fn report(orders: &impl OrderStore)) is often better than &dyn.
Check yourself
Section titled “Check yourself”What is the difference between
fn f(rule: &impl PriceRule)andfn f(rule: &dyn PriceRule)?Show the answer
The first is static dispatch: the compiler makes a copy of
ffor each concrete type, and calls go directly to the method. The second is dynamic dispatch through a vtable, as in Java. Both accept any type that implements the trait.Can you implement your own trait for
String?Show the answer
Yes. Your trait is from your crate, so the orphan rule allows it. You cannot implement a standard library trait such as
DisplayforString, because neither is yours.How does Rust share fields between types without inheritance?
Show the answer
Composition: put the shared fields in their own struct, and give each type a field of that struct. Shared behavior goes in a trait, often with default methods that use one accessor method.
An agent wrote
trait UserServiceandstruct UserServiceImpl, with no other implementation. What do you ask?Show the answer
What the trait is for. If no test or caller uses another implementation, remove the trait and keep the struct. A trait can be added later when it is needed.