UML クラス図とは、システムに登場するクラスの「名前・属性・操作」と、クラス同士の関係を1枚に表す設計図です。 書き方の中心は、クラスを3段の箱で描くことと、関係を6種類の線(関連・集約・コンポジション・汎化・実現・依存)で描き分けることの2つです。

この記事では、記号と関係線を 「意味・線の形・Java コードでの対応・Mermaid 記法・PlantUML 記法」 の一覧表にまとめ、すべての図を mermaid-cli と PlantUML で実際に描画できることを確認しました。さらに、動く Java / TypeScript のコードからクラス図を起こす手順、集約とコンポジションの見分け方、矢印の向きなどの よくある誤りの直す前と後、AI に描かせるときの指示例まで紹介します。

検証環境: @mermaid-js/mermaid-cli 12.0.0(mermaid 12.0.0)、PlantUML 1.2026.8、Java 21.0.10、TypeScript 7.0.2、Node.js 22。

クラス図の書き方の早見表

描きたいもの UML の記号 Mermaid PlantUML
クラス 3区画の箱(名前/属性/操作) class Order { ... } class Order { ... }
public / private + / - +total() int / -String id + total() : int / - id : String
protected / package # / ~ #orderedAt / ~note # orderedAt / ~ note
static(クラスメンバー) 下線 末尾に $ {static}
abstract(抽象) 斜体 操作の末尾に *、クラスは <<abstract>> {abstract} / abstract class
インターフェース «interface» <<interface>> interface Notifier
ステレオタイプ «entity» など <<entity>> class Order <<entity>>
ジェネリクス List<Member> List~Member~ List<Member>
多重度 1、0..1、0..*、1..* Order *-- "1..*" OrderLine 同じ書き方
関係の名前 線の上のラベル A --> B : 名前 同じ書き方

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「UML クラス図の書き方|記号・関係線6種の一覧とMermaid/PlantUML」

関係線6種の一覧(意味・線・Java・Mermaid・PlantUML)

関係 意味 線の形 Java での対応 Mermaid PlantUML
関連 相手を知っていて使い続ける 実線(矢印で参照の向き) フィールドに持つ private Member orderedBy; Order --> Member Order --> Member
集約 全体と部分。部分は単独でも存在できる 実線+白抜きのひし形(全体側) 外から受け取ってコレクションに持つ team.add(member) Team o-- Member Team o-- Member
コンポジション 全体と部分。部分は全体と運命をともにする 実線+黒塗りのひし形(全体側) 内部で生成して外に出さない lines.add(new OrderLine(...)) Order *-- OrderLine Order *-- OrderLine
汎化 「〜の一種」(継承) 実線+白抜き三角(親側) class PremiumMember extends Member PremiumMember --|> Member PremiumMember --|> Member
実現 インターフェースを実装する 破線+白抜き三角(インターフェース側) class MailNotifier implements Notifier MailNotifier ..|> Notifier MailNotifier ..|> Notifier
依存 一時的に使う(変更の影響を受ける) 破線+開いた矢印 引数・ローカル変数・戻り値で使うだけ OrderService ..> TaxCalculator OrderService ..> TaxCalculator

覚え方のポイント:

  • 三角は「親・インターフェース」側、ひし形は「全体」側に付きます。
  • 実線は「持っている(構造)」、破線は「使う・約束する」。汎化と実現は、実線か破線かだけが違います。
  • Mermaid と PlantUML は関係線の記法がほぼ同じです。どちらも Member <|-- PremiumMember のように左右を逆にも書けます。

上の6種を1つの図にしたコードです(mermaid-cli と PlantUML の両方で描画を確認)。

classDiagram
  direction LR
  Order --> "1" Member : 関連(注文者)
  Team o-- "0..*" Member : 集約
  Order *-- "1..*" OrderLine : コンポジション
  PremiumMember --|> Member : 汎化
  MailNotifier ..|> Notifier : 実現
  OrderService ..> TaxCalculator : 依存
@startuml
left to right direction
Order --> "1" Member : 関連(注文者)
Team o-- "0..*" Member : 集約
Order *-- "1..*" OrderLine : コンポジション
PremiumMember --|> Member : 汎化
MailNotifier ..|> Notifier : 実現
OrderService ..> TaxCalculator : 依存
interface Notifier
@enduml

クラスの箱の書き方:3区画・可視性・static・abstract

クラスは上から「クラス名」「属性」「操作」の3区画で描きます。属性は 可視性 名前 : 型、操作は 可視性 名前(引数 : 型) : 戻り値の型 が UML の書き方です(Mermaid は 型 名前 の順でも書けます)。

可視性 記号 Java TypeScript
public + public public(省略時)
private - private private / #field
protected # protected protected
package ~ 修飾子なし ―

Mermaid:

classDiagram
  class Order {
    <<entity>>
    -String id
    -int status
    #LocalDateTime orderedAt
    ~String note
    +int MAX_LINES$
    +addLine(Product product, int qty) void
    +total() int
    +of(String id)$ Order
  }
  class Shape {
    <<abstract>>
    +area()* double
  }
  class Notifier {
    <<interface>>
    +send(String to, String message) void
  }

PlantUML:

@startuml
class Order <<entity>> {
  - id : String
  - status : int
  # orderedAt : LocalDateTime
  ~ note : String
  + {static} MAX_LINES : int
  + addLine(product : Product, qty : int) : void
  + total() : int
  + {static} of(id : String) : Order
}
abstract class Shape {
  + {abstract} area() : double
}
interface Notifier {
  + send(to : String, message : String) : void
}
@enduml

描画すると、Mermaid では static が下線、abstract が斜体、<<entity>> などのステレオタイプがクラス名の上に «entity» として表示されました。PlantUML では可視性の記号がアイコン(例:- は四角、+ は丸)で表示されます。

多重度の書き方

多重度 意味 例
1 ちょうど1 注文には注文者が1人
0..1 0か1 会員の配送先(未登録もあり)
0..*(*) 0以上 チームのメンバー
1..* 1以上 注文の明細(最低1行)
2..5 範囲指定 備品の貸出上限

多重度は「相手側の端」に書きます。 Order *-- "1..*" OrderLine なら「1つの注文に明細が1件以上」と読みます。

コードからクラス図を起こす(Java)

次の Java を java OrderApp.java で実行し、動作を確認してからクラス図にしました。

import java.util.ArrayList;
import java.util.List;

public class OrderApp {
    // 汎化の親
    static class Member {
        private final String name;
        Member(String name) { this.name = name; }
        String getName() { return name; }
        int discountRate() { return 0; }
    }
    // 汎化(extends)
    static class PremiumMember extends Member {
        PremiumMember(String name) { super(name); }
        @Override int discountRate() { return 10; }
    }
    // 集約:Team は Member を外から受け取って持つ。Team が消えても Member は残る
    static class Team {
        private final List<Member> members = new ArrayList<>();
        void add(Member m) { members.add(m); }
        int size() { return members.size(); }
    }
    record Product(String name, int price) {}
    // コンポジション:OrderLine は Order の中で作られ、Order と運命をともにする
    static class OrderLine {
        private final Product product;   // 関連(OrderLine → Product)
        private final int qty;
        private OrderLine(Product product, int qty) { this.product = product; this.qty = qty; }
        int subtotal() { return product.price() * qty; }
    }
    static class Order {
        private final Member orderedBy;                      // 関連(Order → Member、多重度 1)
        private final List<OrderLine> lines = new ArrayList<>(); // コンポジション(1..*)
        Order(Member orderedBy) { this.orderedBy = orderedBy; }
        void addLine(Product p, int qty) { lines.add(new OrderLine(p, qty)); } // 部品は内部で生成
        int total() { return lines.stream().mapToInt(OrderLine::subtotal).sum(); }
        Member getOrderedBy() { return orderedBy; }
    }
    // 実現(implements)
    interface Notifier { void send(String to, String message); }
    static class MailNotifier implements Notifier {
        public void send(String to, String message) { System.out.println("mail to " + to + ": " + message); }
    }
    static class TaxCalculator {
        int withTax(int amount) { return amount * 110 / 100; }
    }
    // 依存:TaxCalculator は引数で受け取って使うだけ(フィールドに持たない)
    // 関連:Notifier はフィールドに持つ
    static class OrderService {
        private final Notifier notifier;
        OrderService(Notifier notifier) { this.notifier = notifier; }
        int checkout(Order order, TaxCalculator tax) {
            int base = order.total() * (100 - order.getOrderedBy().discountRate()) / 100;
            int amount = tax.withTax(base);
            notifier.send(order.getOrderedBy().getName(), "合計 " + amount + " 円");
            return amount;
        }
    }

    public static void main(String[] args) {
        Member hana = new PremiumMember("hana");
        Team team = new Team();
        team.add(hana);
        Order order = new Order(hana);
        order.addLine(new Product("ノート", 300), 2);
        order.addLine(new Product("ペン", 150), 4);
        int amount = new OrderService(new MailNotifier()).checkout(order, new TaxCalculator());
        System.out.println("team size=" + team.size() + ", total=" + order.total() + ", amount=" + amount);
    }
}

実行結果(Java 21.0.10、-Dstdout.encoding=UTF-8 を指定):

mail to hana: 合計 1188 円
team size=1, total=1200, amount=1188

コードの書き方と関係線の対応は次のとおりです。

コード 関係 判断の決め手
class PremiumMember extends Member 汎化 extends
class MailNotifier implements Notifier 実現 implements
private final Member orderedBy; 関連(多重度 1) フィールドに持ち続ける
private final List<OrderLine> lines + new OrderLine(...) を内部で実行 コンポジション 部品を 自分で作り、外に渡さない
private final List<Member> members + add(Member m) 集約 部品を 外から受け取る。Team がなくても Member は残る
checkout(Order order, TaxCalculator tax) 依存 引数で受け取って使うだけで、フィールドに持たない
private final Notifier notifier; 関連(インターフェースへ) 具体クラスではなくインターフェースを持つ

Mermaid で描いたクラス図:

classDiagram
  class Member {
    -String name
    +getName() String
    +discountRate() int
  }
  class PremiumMember {
    +discountRate() int
  }
  class Team {
    -List~Member~ members
    +add(Member m) void
  }
  class Order {
    -Member orderedBy
    -List~OrderLine~ lines
    +addLine(Product p, int qty) void
    +total() int
  }
  class OrderLine {
    -Product product
    -int qty
    +subtotal() int
  }
  class Product {
    +String name
    +int price
  }
  class Notifier {
    <<interface>>
    +send(String to, String message) void
  }
  class MailNotifier
  class TaxCalculator {
    +withTax(int amount) int
  }
  class OrderService {
    -Notifier notifier
    +checkout(Order order, TaxCalculator tax) int
  }
  Member <|-- PremiumMember
  Team o-- "0..*" Member
  Order "0..*" --> "1" Member : orderedBy
  Order *-- "1..*" OrderLine
  OrderLine "0..*" --> "1" Product
  Notifier <|.. MailNotifier
  OrderService --> "1" Notifier
  OrderService ..> TaxCalculator
  OrderService ..> Order

PlantUML で描いたクラス図(hide empty members で空の区画を省略):

@startuml
hide empty members
class Member {
  - name : String
  + getName() : String
  + discountRate() : int
}
class PremiumMember {
  + discountRate() : int
}
class Team {
  - members : List<Member>
  + add(m : Member) : void
}
class Order {
  - orderedBy : Member
  - lines : List<OrderLine>
  + addLine(p : Product, qty : int) : void
  + total() : int
}
class OrderLine {
  - product : Product
  - qty : int
  + subtotal() : int
}
class Product <<record>> {
  + name : String
  + price : int
}
interface Notifier {
  + send(to : String, message : String) : void
}
class MailNotifier
class TaxCalculator {
  + withTax(amount : int) : int
}
class OrderService {
  - notifier : Notifier
  + checkout(order : Order, tax : TaxCalculator) : int
}
Member <|-- PremiumMember
Team o-- "0..*" Member
Order "0..*" --> "1" Member : orderedBy
Order *-- "1..*" OrderLine
OrderLine "0..*" --> "1" Product
Notifier <|.. MailNotifier
OrderService --> "1" Notifier
OrderService ..> TaxCalculator
OrderService ..> Order
@enduml

List<OrderLine> を持つフィールドは、属性として書くか関係線で描くかのどちらかにそろえると読みやすくなります。上の図では練習のため両方を書いていますが、実務では「関係線で描いたものは属性から外す」ルールにすることが多いです。

TypeScript でも対応は同じ

// order.ts — TypeScript でも対応は同じ
interface Notifier {                                   // インターフェース
  send(to: string, message: string): void;
}
class MailNotifier implements Notifier {               // 実現
  send(to: string, message: string): void { console.log(`mail to ${to}: ${message}`); }
}
class Member {
  constructor(private readonly name: string) {}
  getName(): string { return this.name; }
  discountRate(): number { return 0; }
}
class PremiumMember extends Member {                   // 汎化
  override discountRate(): number { return 10; }
}
class Team {
  private readonly members: Member[] = [];             // 集約(外から受け取る)
  add(m: Member): void { this.members.push(m); }
}
class OrderLine {
  constructor(readonly productName: string, readonly price: number, readonly qty: number) {}
}
class Order {
  private readonly lines: OrderLine[] = [];            // コンポジション(内部で生成)
  constructor(private readonly orderedBy: Member) {}   // 関連
  addLine(name: string, price: number, qty: number): void { this.lines.push(new OrderLine(name, price, qty)); }
  total(): number { return this.lines.reduce((s, l) => s + l.price * l.qty, 0); }
  get member(): Member { return this.orderedBy; }
}
class TaxCalculator { withTax(n: number): number { return Math.floor(n * 1.1); } }
class OrderService {
  constructor(private readonly notifier: Notifier) {}  // 関連(インターフェースに向ける)
  checkout(order: Order, tax: TaxCalculator): number { // 依存(引数で使うだけ)
    const amount = tax.withTax(order.total() * (100 - order.member.discountRate()) / 100);
    this.notifier.send(order.member.getName(), `合計 ${amount} 円`);
    return amount;
  }
}
const order = new Order(new PremiumMember('hana'));
order.addLine('ノート', 300, 2);
order.addLine('ペン', 150, 4);
new Team().add(order.member);
console.log(new OrderService(new MailNotifier()).checkout(order, new TaxCalculator()));
$ tsc --strict --target es2022 --module nodenext --outDir out order.ts && node out/order.js
mail to hana: 合計 1188 円
1188

TypeScript の constructor(private readonly x: T)(パラメータプロパティ)はフィールドを宣言するのと同じなので、クラス図では -x : T の属性、または関連として描きます。

集約とコンポジションの見分け方

いちばん迷いやすいのがこの2つです。次の3つの質問に「はい」が多ければコンポジションです。

質問 コンポジション(*--) 集約(o--)
部分は全体の中で作られる? はい(new OrderLine を Order の中で実行) いいえ(外から受け取る)
全体を消したら部分も消える? はい(明細だけ残っても意味がない) いいえ(チームを解散しても会員は残る)
部分は同時に2つの全体に属せない? はい(1つの明細は1つの注文だけ) いいえ(会員は複数チームに所属できる)

迷ったら普通の関連(-->)で描き、多重度で表すのも正しい選択です。UML 2 の仕様でも集約(白ひし形)の意味は厳密には定められていないため、チームで「白ひし形は使わない」と決めている現場もあります。

よくある誤りと直す前と後

classDiagram
  Order o-- OrderLine
  Member --|> PremiumMember
  MailNotifier --|> Notifier
  OrderService --> TaxCalculator

↓ 直した後:

classDiagram
  Order *-- "1..*" OrderLine
  PremiumMember --|> Member
  MailNotifier ..|> Notifier
  OrderService ..> TaxCalculator
  class Notifier {
    <<interface>>
  }
直す前 何が起きているか 直した後
Order o-- OrderLine 明細は注文の中で作られ、注文と一緒に消えるのに集約になっている Order *-- "1..*" OrderLine
Member --|> PremiumMember 三角が子の側を向いていて「会員はプレミアム会員の一種」と読めてしまう PremiumMember --|> Member(三角は親側)
MailNotifier --|> Notifier 実線なので「継承」になっている MailNotifier ..|> Notifier(破線で実現)
OrderService --> TaxCalculator 引数で使うだけなのに関連(フィールドに持つ)になっている OrderService ..> TaxCalculator(依存)

どちらも mermaid-cli と PlantUML で描画を確認しました(PlantUML 版は classDiagram 行を外し、@startuml / @enduml で囲んで interface Notifier を追加したもの)。

ツールで起きやすいつまずき

症状 原因 対処
Mermaid で -List<Member> members が -List members と表示される Mermaid ではジェネリクスを ~ で書く(エラーにならず <Member> が消える) -List~Member~ members
Mermaid で static や abstract が反映されない 記号の位置が違う static は末尾に $、抽象操作は () の後ろに *
PlantUML でインターフェースが普通のクラスになる class で宣言している interface Notifier と宣言する
PlantUML の -testdot で Dot executable does not exist Graphviz が入っていない 今回の 1.2026.8 ではクラス図は Graphviz なしで描画できた。描けない版では !pragma layout smetana を追加するか Graphviz を入れる

設計レビューで見るポイント

クラス図は「描けているか」より 「設計の問題に気づけるか」 に価値があります。レビューでは次の点を確認します。

観点 図のどこを見るか 気づけること
依存の向き 矢印が集中しているクラス 変更の影響が大きいクラス。インターフェースを挟む候補
循環 A → B → A の矢印 片方を依存(引数)にする、共通部分を切り出す
具体クラスへの関連 OrderService --> MailNotifier のような線 インターフェース(Notifier)に向けるとテストで差し替えやすい
巨大なクラス 属性・操作が多い箱 責務の分割を検討
多重度 * の付け忘れ、1 と 0..1 の違い 未登録を許すか、必須かの仕様確認
継承の深さ 三角が何段も続く コンポジションで表せないか

1枚にすべてを描かないのもコツです。レビューの目的(ドメインの構造、依存の向き、特定機能の周辺)ごとに、クラスを絞った図を複数作るほうが伝わります。

AI にクラス図を描かせる指示例

生成 AI はコードからクラス図を起こすのが得意ですが、関係の判定ルールを指示しないと、関連と依存、集約とコンポジションが混ざりやすくなります。次のように条件を渡すと安定します。

次の Java コードから、Mermaid の classDiagram を作成してください。
条件:
- extends は汎化(--|>)、implements は実現(..|>)
- フィールドで保持する参照は関連(-->)。多重度を "1" や "0..*" で書く
- クラス内部で new して外に渡さない部品はコンポジション(*--)
- 外から受け取ってコレクションに保持するものは集約(o--)
- 引数・ローカル変数・戻り値でのみ使うクラスは依存(..>)
- 可視性は + - # ~、static は $、抽象メソッドは * を付ける
- ジェネリクスは List~Member~ のように ~ で書く
- 関係線で描いたフィールドは属性の区画から省く
- 最後に、各関係をそう判定した根拠(該当するコード行)を表で示す
(ここにコードを貼る)

「判定の根拠を表で出させる」と、人がレビューするときに誤りを見つけやすくなります。生成された Mermaid は mermaid-cli(npx -p @mermaid-js/mermaid-cli mmdc -i diagram.mmd -o diagram.svg)で描画して確認できます。

よくある質問

Q. クラス図と ER 図はどう違いますか? A. ER 図はデータベースのテーブルと外部キーの関係、クラス図はプログラムのクラスと、振る舞い(操作)を含む関係を表します。クラス図には継承・インターフェース・依存がありますが、ER 図にはありません。

Q. 属性や操作はすべて書く必要がありますか? A. 図の目的に必要なものだけで十分です。概念を話し合う段階ではクラス名と関係だけ、詳細設計では型と可視性まで、と段階を分けると描きやすくなります。

Q. Mermaid と PlantUML はどちらを使えばよいですか? A. GitHub や多くの Markdown ツールでそのまま表示したいなら Mermaid、レイアウトの細かい調整や UML の表現力を重視するなら PlantUML が向いています。関係線の記法はほぼ共通なので、後から移し替えるのも簡単です。

Q. 関連の矢印はいつ付けますか? A. 片方だけが相手を参照する(ナビゲーションできる)なら矢印付き、お互いに参照するなら矢印なしの実線(--)で描きます。コードでフィールドを持っている側から矢印を出すと考えるとわかりやすいです。

まとめ

  • クラスは3区画(名前・属性・操作)、可視性は + - # ~、static は下線、abstract は斜体
  • 関係は6種:関連(実線)、集約(白ひし形)、コンポジション(黒ひし形)、汎化(実線+三角)、実現(破線+三角)、依存(破線矢印)
  • 三角は親・インターフェース側、ひし形は全体側に付ける
  • フィールドで持つ=関連、引数で使う=依存、内部で作る部品=コンポジション
  • Mermaid はジェネリクスを ~ で書く(<> だと型が消える)
  • レビューでは依存の向き・循環・具体クラスへの関連を見る
  • AI には判定ルールと根拠の表を指示すると精度が上がる

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「UML クラス図の書き方|記号・関係線6種の一覧とMermaid/PlantUML」

株式会社RJCでは、コードと設計図を行き来しながら設計の考え方を、プロジェクトの中で仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。

WE ARE HIRING

RJCで一緒に開発しながら、
成長を楽しみませんか?

RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!

  • 127日年間休日
  • 12時間平均残業時間
  • 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!

ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?