A lightweight, zero-dependency object query language for Java, with a fluent SQL-like API inspired by C# LINQ.
中文 / English
Linq.J makes Java collection queries as intuitive as writing SQL. JOIN, WHERE, GROUP BY, ORDER BY — all in one fluent chain. Code is documentation.
// SELECT dept.name, AVG(salary), COUNT(*)
// FROM employees INNER JOIN departments ON dept.id = emp.deptId
// WHERE salary > 5000
// GROUP BY dept.name ORDER BY salary DESC
var result = Linq.from(employees)
.innerJoin(departments, Dept::getId, Emp::getDeptId)
.where(Emp::getSalary, salary -> salary > 5000)
.groupBy(Dept::getName)
.select(
Columns.of(Dept::getName, "department"),
Columns.avg(Emp::getSalary, "avgSalary"),
Columns.count("headcount")
)
.orderByDesc(Emp::getSalary)
.toList(DeptStats.class);| Dependencies | Package Size | Min Version | License |
|---|---|---|---|
| 0 | ~50 KB | JDK 8+ | MIT |
Lightweight, powerful, and elegant — tackle the most complex queries with minimal code.
| ⚡ Zero Deps · Ultra Lightweight | No third-party runtime dependencies, only ~50 KB. Drop it in and go — zero project overhead. |
| 🔗 SQL-Like Fluent API | Select, Join, Where, Group By, Order By, Limit… operate on in-memory data using familiar SQL thinking. |
| 🛡️ Type-Safe | Column operations via lambda method references — field names are checked at compile time, IDE auto-completes, no magic strings. |
| 🗃️ Multi-Source | List, Array, SQL result sets, CSV/JSON/XML, Stream… any in-memory collection is queryable. |
| 🔌 Pluggable Engine | Abstract Engine interface; the default EruptEngine ships Hash Join and other efficient strategies, fully customizable. |
| 📊 Rich Aggregations | COUNT, SUM, AVG, MAX, MIN, COUNT DISTINCT… full aggregation capability, works seamlessly with Group By. |
Linq.J maps each SQL clause to a chainable method, evaluated in this order:
FROM → JOIN → WHERE → GROUP BY → SELECT → HAVING → ORDER BY → LIMIT → toList
| Clause | Methods |
|---|---|
| JOIN | leftJoin() · rightJoin() · innerJoin() · fullJoin() |
| WHERE | eq() · ne() · like() · between() · in() · notIn() · isNull() · gt() / lt() / gte() / lte() · where(λ) |
| SELECT | select(Class) · select(Field...) · selectAs() · count() · sum() · avg() · max() / min() · countDistinct() |
| TERMINAL | toList(Class) · one(Class) · first(Class) · toMaps() · toMap() · count() · exists() |
<dependency>
<groupId>xyz.erupt</groupId>
<artifactId>linq.j</artifactId>
<version>${LATEST}</version>
</dependency>Linq.J resolves field names from lambda method references via SerializedLambda. Your data classes must expose getter methods — Lombok @Getter is recommended:
@Getter
public class User {
private Long id;
private String name;
private Integer age;
}List<User> adults = Linq.from(users)
.where(User::getAge, age -> age >= 18)
.select(User::getName, User::getAge)
.orderBy(User::getAge)
.toList(User.class);Works on primitives and custom objects alike. It::self refers to the element itself for simple-typed sources.
// String filter + sort
var strings = Linq.from("C", "A", "B", "B")
.gt(It::self, "A")
.orderByDesc(It::self)
.toList(String.class); // [C, B, B]
// Number sort
var integers = Linq.from(1, 2, 3, 7, 6, 5)
.orderBy(It::self)
.toList(Integer.class); // [1, 2, 3, 5, 6, 7]Four standard join types, powered by a Hash Join engine, syntax mirrors SQL exactly.
// Left Join + multi-table select
Linq.from(source)
.leftJoin(target, Target::getId, Source::getId)
.select(Source.class)
.select(Target::getName)
.toList(Result.class);
// Inner Join + Where + Distinct
var names = Linq.from(data)
.innerJoin(target, Target::getId, Data::getId)
.like(Data::getName, "a")
.select(Data::getName)
.distinct()
.orderBy(Data::getName)
.toList(String.class);From simple equality to custom multi-field predicates.
Linq.from(source).eq(User::getName, "Thanos"); // exact match
Linq.from(source).between(User::getId, 1, 100); // range (inclusive)
Linq.from(source).in(User::getId, 1, 2, 3); // in list
Linq.from(source).like(User::getName, "admin"); // contains
Linq.from(source).where(User::getId, id -> id >= 5); // single-field predicate
// multi-field predicate over the whole row
Linq.from(source).where(row -> {
String name = row.get(User::getName);
Integer age = (Integer) row.get(User::getAge);
return "admin".equals(name) || age > 18;
});Group By + aggregate functions + Having — generating reports is effortless.
Linq.from(orders)
.groupBy(Order::getCategory)
.select(
Columns.of(Order::getCategory, "name"),
Columns.min(Order::getDate, "earliest"),
Columns.avg(Order::getPrice, "avgPrice"),
Columns.count("total"),
Columns.countDistinct(Order::getBuyer, "uniqueBuyers")
)
.having("total", total -> ((Number) total).intValue() > 10)
.orderBy(Order::getPrice)
.toList(CategoryStats.class);Query results map to objects, List, Map, or a single value — plus lightweight terminals.
List<User> list = Linq.from(source).toList(User.class); // List<T>
User one = Linq.from(source).eq(User::getId, 1).one(User.class); // exactly one (throws if >1)
User first = Linq.from(source).orderBy(User::getId).first(User.class); // first or null
List<Map<String, Object>> maps = Linq.from(source).toMaps(); // List<Map>
Map<String, Object> map = Linq.from(source).toMap(); // single Map
int total = Linq.from(source).gt(User::getAge, 18).count(); // row count
boolean any = Linq.from(source).eq(User::getName, "root").exists(); // any match?Linq.J operates on any in-memory collection. Feed it data from anywhere:
| Source | Usage |
|---|---|
| List / Array | Linq.from(list) or Linq.from("A", "B", "C") |
| SQL Result | Load via JDBC / MyBatis / JPA, then query with Linq |
| CSV / XML / JSON | Parse into objects, then query with Linq |
| Redis / MongoDB | Fetch results, then query with Linq |
| Stream / File | Collect into a list, then query with Linq |
| RPC Response | Feign / Dubbo / gRPC results, then query with Linq |
A clean layered design — query building and execution are decoupled and independently extensible:
┌───────────────────────────────────────────────────────┐
│ Fluent API Layer │
│ Select · Join · Where · GroupBy · OrderBy · Write │
├───────────────────────────────────────────────────────┤
│ Query Model Layer │
│ Linq (Facade) · Dql (State) · Column · Row │
├───────────────────────────────────────────────────────┤
│ Execution Engine Layer │
│ Engine (Abstract) → EruptEngine (Default) │
├───────────────────────────────────────────────────────┤
│ Lambda Resolution Layer │
│ SFunction · LambdaSee · SerializedLambda │
└───────────────────────────────────────────────────────┘
Pluggable Engine — supply your own execution strategy per query:
Linq linq = Linq.from(data);
linq.setEngine(new MyCustomEngine());
linq.where(User::getAge, age -> age >= 18).toList(User.class);Parallel materialization — opt in for large datasets (order-preserving, identical results):
Linq.from(bigList).parallel()
.where(User::getAge, age -> age >= 18)
.toList(User.class);| Scenario | Description |
|---|---|
| RPC Result Association | Join results from Feign / Dubbo / gRPC calls in memory instead of multiple DB roundtrips |
| Heterogeneous Data | Unify and query data from Redis, MongoDB, MySQL in one place |
| Post-SQL Processing | Further filter, sort, and aggregate database query results in code |
| In-Memory Pagination | Merge multiple result sets, then sort, aggregate, and paginate |
| Object Mapping | Semantic, readable object transformation and projection |
| Cross-Source Federation | Federated queries across different data sources at the application layer |
|
Traditional Java Map<String, List<Order>> grouped = new HashMap<>();
for (Order o : orders) {
if (o.getAmount() > 100) {
grouped.computeIfAbsent(
o.getCategory(),
k -> new ArrayList<>()
).add(o);
}
}
Map<String, Double> avgMap = new HashMap<>();
for (var entry : grouped.entrySet()) {
double sum = 0;
for (var o : entry.getValue()) {
sum += o.getAmount();
}
avgMap.put(entry.getKey(),
sum / entry.getValue().size());
}
// Still need sorting... more code... |
With Linq.J var result = Linq.from(orders)
.where(Order::getAmount, a -> a > 100)
.groupBy(Order::getCategory)
.select(
Columns.of(Order::getCategory, "cat"),
Columns.avg(Order::getAmount, "avg"),
Columns.count("cnt")
)
.orderByDesc(Order::getAmount)
.toList(Stats.class); |
-
HAVINGclause support - Group column formatting (
group by date(created_at)) - Set operations:
UNION ALL,UNION,INTERSECT,EXCEPT,UNION BY NAME - Window functions (
ROW_NUMBER,RANK,DENSE_RANK, …)
Contributions are welcome — feel free to open issues and pull requests.
MIT © Linq.J