Classes and Objects¶
Onion is an object-oriented language with full support for classes, inheritance, and interfaces.
Class Definition¶
Basic Class¶
Define a class with the class keyword:
class Person {
val name: String
var age: Int
public:
def this(n: String, a: Int) {
this.name = n
this.age = a
}
def greet: String = "Hello, I'm " + this.name
}
Creating Objects¶
Instantiate objects with the new keyword:
Primary Constructors¶
The concise way to declare a class: parameters after the class name form
the primary constructor. val/var parameters become public fields
(final/mutable) assigned automatically; plain parameters exist only in
the constructor (useful for superclass arguments). Defaults and named
arguments work like everywhere else.
class Point(val x: Int, val y: Int) {
public:
def dist(): Int { return this.x * this.x + this.y * this.y }
}
class Conf(val host: String = "localhost", var port: Int = 8080)
class Animal(val name: String)
class Dog(name: String, val breed: String) : Animal(name) // body-less is fine
val p = new Point(3, 4) // p.x, p.y readable; p.x = 9 is an error (val)
val c = new Conf(port = 9090) // host defaults to "localhost"
def this(...) constructors remain available for classes that need
explicit bodies or multiple overloads.
Fields¶
Instance Fields¶
Declare instance fields with val (immutable) or var (mutable), and access them via this.field:
class Counter {
var count: Int
public:
def this {
this.count = 0
}
def increment {
this.count = this.count + 1
}
def getCount: Int = this.count
}
Access Modifiers¶
Members are private by default. Use public: to mark public members:
class BankAccount {
var balance: Double // Private (default)
val accountNumber: String // Private
public:
val owner: String // Public
def this(owner: String, initial: Double) {
this.owner = owner
this.balance = initial
this.accountNumber = "UNKNOWN"
}
def deposit(amount :Double) { // Public method
this.balance = this.balance + amount
}
def getBalance: Double = this.balance // Public method
}
Static Members¶
Static members belong to the class, not instances:
class MathUtils {
static val PI: Double = 3.14159
public:
static def square(x: Double): Double = x * x
static def circleArea(radius: Double): Double = MathUtils::PI * radius * radius
}
// Access static members with ::
val pi: Double = MathUtils::PI
val area: Double = MathUtils::circleArea(5.0)
Constructors¶
Default Constructor¶
Define constructors with def this:
class Point {
val x: Int
val y: Int
public:
def this(x: Int, y: Int) {
this.x = x
this.y = y
}
}
val point: Point = new Point(10, 20)
Multiple Constructors¶
Overload constructors for different initialization patterns:
class Rectangle {
val width: Int
val height: Int
public:
def this {
this.width = 0
this.height = 0
}
def this(size: Int) {
this.width = size
this.height = size
}
def this(w: Int, h: Int) {
this.width = w
this.height = h
}
}
val rect1: Rectangle = new Rectangle()
val rect2: Rectangle = new Rectangle(10)
val rect3: Rectangle = new Rectangle(10, 20)
Calling Super Constructors¶
To call a superclass constructor, add a super-initializer list: def this(args): (superArgs) { ... }.
Methods¶
Instance Methods¶
Methods that operate on instance data:
class Circle {
val radius: Double
public:
def this(r: Double) {
this.radius = r
}
def area: Double = 3.14159 * this.radius * this.radius
def circumference: Double = 2.0 * 3.14159 * this.radius
}
val circle: Circle = new Circle(5.0)
println("Area: " + circle.area())
Method Overloading¶
Multiple methods with the same name but different parameters:
class Printer {
public:
def print(value :Int) {
println("Int: " + value)
}
def print(value :String) {
println("String: " + value)
}
def print(value :Double) {
println("Double: " + value)
}
}
val printer: Printer = new Printer
printer.print(42)
printer.print("Hello")
printer.print(3.14)
Getter and Setter Methods¶
class Person {
var name: String
var age: Int
public:
def getName: String = this.name
def setName(name :String) {
this.name = name
}
def getAge: Int = this.age
def setAge(age :Int) {
if age >= 0 {
this.age = age
}
}
}
The self Reference¶
Access the current instance with self:
import {
javax.swing.JButton;
java.awt.event.ActionEvent;
java.awt.event.ActionListener;
}
class ButtonHandler <: ActionListener {
public:
def actionPerformed(event :ActionEvent) {
val button: JButton = event.getSource() as JButton
button.addActionListener(self) // Reference to this instance
}
}
this and self are only available in instance contexts; static methods and static fields cannot reference them.
Next Steps¶
- Inheritance - Extending classes and implementing interfaces
- Java Interoperability - Working with Java classes
- Examples - Object-oriented examples
Records¶
Records are concise immutable data classes with generated equals,
hashCode, toString and copy:
record Point(x: Int, y: Int)
val p = new Point(1, 2)
p.x() // component access (methods)
p.copy(y = 9) // partial copy with named arguments
p.copy() // full clone
p.copy(5, 6) // positional copy
Records work with select pattern matching when combined with sealed
interfaces, and take type parameters:
record Pair[A, B](first: A, second: B)
val p = new Pair[String, Integer]("gen", 9)
val (s, n) = p // destructuring declaration
p.copy(second = 42) // named-argument copy
A record can also carry a { ... } body of methods — instance methods, static
factories, private helpers, and operator methods — just like a class or enum.
The methods see the generated component accessors:
record Fraction(num: Int, den: Int) {
public:
static def of(n: Int, d: Int): Fraction {
val g = gcd(Math::abs(n), d)
return new Fraction(n / g, d / g)
}
def plus(o: Fraction): Fraction = // backs the `+` operator
Fraction::of(num() * o.den() + o.num() * den(), den() * o.den())
def toDouble(): Double = (num() as Double) / (den() as Double)
private:
static def gcd(a: Int, b: Int): Int { ... }
}
val third = Fraction::of(1, 3)
val one = third + third + third // exactly 1/1
Generic Classes¶
Classes can take type parameters in []. A parameter is available
throughout the body as an ordinary type:
When the expected type pins the type argument, the constructor infers it, so
new Box(...) needs no [T] (the "diamond"). Explicit type arguments still
work, and both forms are equivalent:
val b: Box[String] = new Box("x") // T inferred as String
val n: Box[Integer] = new Box(9) // T inferred as Integer
val b2: Box[String] = new Box[String]("y") // explicit — same result
With no expected type to infer from, the bare generic is rejected — supply an expected type or explicit type arguments:
// val bad = new Box("x") // ERROR E0066: raw generic type Box —
// write `new Box[String]("x")` or annotate the target
Type arguments are invariant (Box[Dog] is not a Box[Animal]); see
Variables and Types.
Operator Overloading¶
Binary operators dispatch to convention methods on the left operand
(Kotlin-style): a + b calls a.plus(b), and likewise - → minus,
* → times, / → div, % → rem. Compound assignment (a += b)
goes through the same method. + keeps string concatenation whenever a
String is involved, and numeric operands keep primitive arithmetic.
class Vec {
val x: Int
val y: Int
public:
def this(x: Int, y: Int) { this.x = x; this.y = y }
def plus(o: Vec): Vec { return new Vec(this.x + o.x, this.y + o.y) }
def times(k: Int): Vec { return new Vec(this.x * k, this.y * k) }
}
val v = new Vec(1, 2) + new Vec(3, 4) // Vec(4, 6)
val w = new Vec(1, 2) * 3 // Vec(3, 6)
Enums¶
Enums compile to standard JVM enums. Constants get name() / ordinal();
values() and valueOf(String) work as in Java. Record-style parameters
make data-carrying enums: each parameter becomes a final field with an
accessor, and constants pass constructor arguments.
enum Color { RED, GREEN, BLUE }
enum Planet(mass: Double) {
MERCURY(3.3e23),
EARTH(5.97e24)
}
println("" + Planet::EARTH.mass())
foreach p: Planet in Planet::values() {
println(p.name() + " = " + p.mass())
}
Planet::valueOf("EARTH") // works with java.lang.Enum.valueOf
Enums can declare methods in access sections after the constant list —
instance methods see the constant's data, static methods see values():
enum Planet(mass: Double) {
MERCURY(3.3e23),
EARTH(5.97e24)
public:
def heavierThan(other: Planet): Boolean {
return this.mass() > other.mass()
}
}
Algebraic data types (case cases)¶
When the cases use the case keyword, each case can carry its own fields —
the enum becomes a full sum-of-products, so an algebraic data type no longer
needs a hand-written sealed interface plus records:
enum Shape {
case Circle(radius: Double)
case Square(side: Double)
case Origin
public:
def area(): Double = select this {
case c is Circle: c.radius() * c.radius() * 3.14
case s is Square: s.side() * s.side()
case o is Origin: 0.0
}
}
val c: Shape = new Circle(2.0)
c.area() // 12.56
Each product case (case Circle(radius: Double)) has typed fields with
accessors; a singleton case (case Origin) is a zero-field case used as
new Origin(). The enum desugars to a sealed interface with one record per
case, so exhaustiveness (E0042) and select pattern matching come for free.
A case-style enum is a sealed hierarchy rather than a java.lang.Enum, so it
does not get values()/valueOf()/ordinal() — use the plain constant form
above when you want those.