or run

npx @tessl/cli init
Log in

Version

Tile

Overview

Evals

Files

Files

docs

asynchronous-caching.mdcache-construction.mdcache-policies.mdfunctional-interfaces.mdindex.mdstatistics.mdsynchronous-caching.md

functional-interfaces.mddocs/

0

# Functional Interfaces

1

2

Caffeine provides several functional interfaces that enable customization of cache behavior including value loading, removal notifications, entry weighing, custom expiration policies, and time/scheduling abstractions.

3

4

## CacheLoader Interface

5

6

The `CacheLoader` interface enables automatic value computation for loading caches.

7

8

```java { .api }

9

@FunctionalInterface

10

public interface CacheLoader<K, V> extends AsyncCacheLoader<K, V> {

11

// Primary loading method

12

V load(K key) throws Exception;

13

14

// Bulk loading method (optional override)

15

default Map<? extends K, ? extends V> loadAll(Set<? extends K> keys) throws Exception {

16

throw new UnsupportedOperationException();

17

}

18

19

// Inherited async methods

20

default CompletableFuture<V> asyncLoad(K key, Executor executor) throws Exception;

21

default CompletableFuture<? extends Map<? extends K, ? extends V>> asyncLoadAll(

22

Set<? extends K> keys, Executor executor) throws Exception;

23

default CompletableFuture<V> asyncReload(K key, V oldValue, Executor executor) throws Exception;

24

25

// Static factory method for bulk loading

26

static <K, V> CacheLoader<K, V> bulk(

27

Function<? super Set<? extends K>, ? extends Map<? extends K, ? extends V>> mappingFunction);

28

}

29

```

30

31

### Basic CacheLoader Implementation

32

33

```java

34

// Simple lambda-based loader

35

CacheLoader<String, String> simpleLoader = key -> "loaded_" + key.toUpperCase();

36

37

LoadingCache<String, String> cache = Caffeine.newBuilder()

38

.maximumSize(1000)

39

.build(simpleLoader);

40

41

String value = cache.get("test"); // Returns "loaded_TEST"

42

```

43

44

### Advanced CacheLoader with Error Handling

45

46

```java

47

CacheLoader<String, UserData> userLoader = new CacheLoader<String, UserData>() {

48

@Override

49

public UserData load(String userId) throws Exception {

50

// Simulate database lookup with potential failures

51

if (userId.startsWith("invalid_")) {

52

throw new IllegalArgumentException("Invalid user ID: " + userId);

53

}

54

55

if (userId.equals("slow_user")) {

56

Thread.sleep(2000); // Simulate slow operation

57

}

58

59

return new UserData(userId, "User " + userId, "user" + userId + "@example.com");

60

}

61

62

@Override

63

public Map<String, UserData> loadAll(Set<? extends String> userIds) throws Exception {

64

Map<String, UserData> result = new HashMap<>();

65

66

// Efficient bulk loading from database

67

List<UserData> users = database.batchFetchUsers(new ArrayList<>(userIds));

68

for (UserData user : users) {

69

result.put(user.getId(), user);

70

}

71

72

return result;

73

}

74

};

75

76

LoadingCache<String, UserData> userCache = Caffeine.newBuilder()

77

.maximumSize(10000)

78

.expireAfterWrite(Duration.ofMinutes(30))

79

.recordStats()

80

.build(userLoader);

81

82

// Single load

83

UserData user = userCache.get("user123");

84

85

// Bulk load - uses efficient loadAll implementation

86

Map<String, UserData> users = userCache.getAll(Set.of("user1", "user2", "user3"));

87

```

88

89

### CacheLoader Bulk Factory Method

90

91

```java

92

// Create a bulk-only loader using the static factory method

93

CacheLoader<String, Product> bulkProductLoader = CacheLoader.bulk(productIds -> {

94

// Only implement bulk loading - single load delegates to bulk

95

return productService.fetchProductsMap(productIds)

96

.stream()

97

.collect(Collectors.toMap(Product::getId, p -> p));

98

});

99

100

LoadingCache<String, Product> productCache = Caffeine.newBuilder()

101

.maximumSize(5000)

102

.expireAfterWrite(Duration.ofHours(1))

103

.build(bulkProductLoader);

104

105

// Both single and bulk loads work efficiently

106

Product single = productCache.get("prod123"); // Delegates to bulk load with single key

107

Map<String, Product> bulk = productCache.getAll(Set.of("prod1", "prod2", "prod3"));

108

```

109

110

## AsyncCacheLoader Interface

111

112

The `AsyncCacheLoader` interface provides asynchronous value loading for async caches.

113

114

```java { .api }

115

@FunctionalInterface

116

public interface AsyncCacheLoader<K, V> {

117

// Primary async loading method

118

CompletableFuture<? extends V> asyncLoad(K key, Executor executor) throws Exception;

119

120

// Bulk async loading (optional override)

121

default CompletableFuture<? extends Map<? extends K, ? extends V>> asyncLoadAll(

122

Set<? extends K> keys, Executor executor) throws Exception {

123

throw new UnsupportedOperationException();

124

}

125

126

// Async reload for refresh operations (optional override)

127

default CompletableFuture<? extends V> asyncReload(

128

K key, V oldValue, Executor executor) throws Exception {

129

return asyncLoad(key, executor);

130

}

131

132

// Static factory methods for bulk loading

133

static <K, V> AsyncCacheLoader<K, V> bulk(

134

Function<? super Set<? extends K>, ? extends Map<? extends K, ? extends V>> mappingFunction);

135

static <K, V> AsyncCacheLoader<K, V> bulk(

136

BiFunction<? super Set<? extends K>, ? super Executor,

137

? extends CompletableFuture<? extends Map<? extends K, ? extends V>>> mappingFunction);

138

}

139

```

140

141

### AsyncCacheLoader Implementation

142

143

```java

144

AsyncCacheLoader<String, String> asyncLoader = (key, executor) -> {

145

return CompletableFuture.supplyAsync(() -> {

146

// Simulate async computation

147

try {

148

Thread.sleep(100);

149

} catch (InterruptedException e) {

150

Thread.currentThread().interrupt();

151

throw new RuntimeException(e);

152

}

153

return "async_loaded_" + key;

154

}, executor);

155

};

156

157

AsyncLoadingCache<String, String> asyncCache = Caffeine.newBuilder()

158

.maximumSize(1000)

159

.buildAsync(asyncLoader);

160

161

CompletableFuture<String> future = asyncCache.get("test");

162

String value = future.join(); // "async_loaded_test"

163

```

164

165

### AsyncCacheLoader with Bulk Loading

166

167

```java

168

AsyncCacheLoader<String, ProductData> productLoader = new AsyncCacheLoader<String, ProductData>() {

169

@Override

170

public CompletableFuture<ProductData> asyncLoad(String productId, Executor executor) {

171

return CompletableFuture.supplyAsync(() -> {

172

return productService.fetchProduct(productId);

173

}, executor);

174

}

175

176

@Override

177

public CompletableFuture<Map<String, ProductData>> asyncLoadAll(

178

Set<? extends String> productIds, Executor executor) {

179

return CompletableFuture.supplyAsync(() -> {

180

// Efficient bulk loading

181

return productService.batchFetchProducts(new ArrayList<>(productIds))

182

.stream()

183

.collect(Collectors.toMap(ProductData::getId, p -> p));

184

}, executor);

185

}

186

187

@Override

188

public CompletableFuture<ProductData> asyncReload(

189

String productId, ProductData oldValue, Executor executor) {

190

return CompletableFuture.supplyAsync(() -> {

191

// Custom refresh logic - may use old value for optimization

192

if (oldValue.getLastModified().isAfter(Instant.now().minus(Duration.ofMinutes(5)))) {

193

return oldValue; // Recent enough, no need to refresh

194

}

195

return productService.fetchProduct(productId);

196

}, executor);

197

}

198

};

199

```

200

201

### AsyncCacheLoader Bulk Factory Methods

202

203

```java

204

// Synchronous bulk loader

205

AsyncCacheLoader<String, Product> syncBulkLoader = AsyncCacheLoader.bulk(

206

productIds -> {

207

// Synchronous bulk loading - will be wrapped in CompletableFuture

208

return productService.fetchProductsSync(productIds)

209

.stream()

210

.collect(Collectors.toMap(Product::getId, p -> p));

211

}

212

);

213

214

// Asynchronous bulk loader with executor

215

AsyncCacheLoader<String, Product> asyncBulkLoader = AsyncCacheLoader.bulk(

216

(productIds, executor) -> {

217

return CompletableFuture.supplyAsync(() -> {

218

// Long-running bulk operation using provided executor

219

return productService.fetchProductsBatch(productIds)

220

.stream()

221

.collect(Collectors.toMap(Product::getId, p -> p));

222

}, executor);

223

}

224

);

225

226

AsyncLoadingCache<String, Product> bulkCache = Caffeine.newBuilder()

227

.maximumSize(10_000)

228

.buildAsync(asyncBulkLoader);

229

230

// Efficient bulk loading through factory method

231

CompletableFuture<Map<String, Product>> productsFuture =

232

bulkCache.getAll(Set.of("prod1", "prod2", "prod3"));

233

```

234

235

## RemovalListener Interface

236

237

The `RemovalListener` interface provides notifications when entries are removed from the cache.

238

239

```java { .api }

240

@FunctionalInterface

241

public interface RemovalListener<K, V> {

242

void onRemoval(K key, V value, RemovalCause cause);

243

}

244

```

245

246

### Basic RemovalListener

247

248

```java

249

RemovalListener<String, String> basicListener = (key, value, cause) -> {

250

System.out.println("Removed: " + key + " -> " + value + " (cause: " + cause + ")");

251

};

252

253

Cache<String, String> cache = Caffeine.newBuilder()

254

.maximumSize(100)

255

.removalListener(basicListener)

256

.build();

257

258

cache.put("key1", "value1");

259

cache.invalidate("key1"); // Prints: Removed: key1 -> value1 (cause: EXPLICIT)

260

```

261

262

### Advanced RemovalListener with Resource Cleanup

263

264

```java

265

RemovalListener<String, DatabaseConnection> connectionListener = (key, connection, cause) -> {

266

System.out.println("Connection removed: " + key + " (cause: " + cause + ")");

267

268

if (connection != null) {

269

try {

270

connection.close();

271

System.out.println("Connection closed for: " + key);

272

} catch (Exception e) {

273

System.err.println("Failed to close connection for " + key + ": " + e.getMessage());

274

}

275

}

276

277

// Log different removal causes

278

switch (cause) {

279

case EXPIRED:

280

metricsCollector.increment("cache.connection.expired");

281

break;

282

case SIZE:

283

metricsCollector.increment("cache.connection.evicted");

284

System.out.println("WARNING: Connection evicted due to cache size limit");

285

break;

286

case EXPLICIT:

287

metricsCollector.increment("cache.connection.manual_removal");

288

break;

289

case COLLECTED:

290

metricsCollector.increment("cache.connection.gc_collected");

291

System.out.println("Connection garbage collected: " + key);

292

break;

293

}

294

};

295

296

Cache<String, DatabaseConnection> connectionCache = Caffeine.newBuilder()

297

.maximumSize(50)

298

.expireAfterAccess(Duration.ofMinutes(30))

299

.removalListener(connectionListener)

300

.build();

301

```

302

303

### RemovalListener vs EvictionListener

304

305

```java

306

// RemovalListener - called asynchronously for ALL removals

307

RemovalListener<String, String> removalListener = (key, value, cause) -> {

308

System.out.println("REMOVAL: " + key + " (cause: " + cause + ")");

309

// Heavy operations like logging, cleanup, external notifications

310

slowExternalService.notifyRemoval(key, value, cause);

311

};

312

313

// EvictionListener - called synchronously only for evictions

314

RemovalListener<String, String> evictionListener = (key, value, cause) -> {

315

if (cause.wasEvicted()) {

316

System.out.println("EVICTION: " + key + " (cause: " + cause + ")");

317

// Lightweight operations only - affects cache performance

318

quickMetrics.recordEviction(cause);

319

}

320

};

321

322

Cache<String, String> cache = Caffeine.newBuilder()

323

.maximumSize(100)

324

.removalListener(removalListener) // Async, all removals

325

.evictionListener(evictionListener) // Sync, evictions only

326

.build();

327

```

328

329

## Weigher Interface

330

331

The `Weigher` interface calculates custom weights for cache entries used in weight-based eviction.

332

333

```java { .api }

334

@FunctionalInterface

335

public interface Weigher<K, V> {

336

int weigh(K key, V value);

337

338

// Static factory methods

339

static <K, V> Weigher<K, V> singletonWeigher();

340

static <K, V> Weigher<K, V> boundedWeigher(Weigher<K, V> delegate);

341

}

342

```

343

344

### String-Based Weigher

345

346

```java

347

Weigher<String, String> stringWeigher = (key, value) -> {

348

// Weight based on string length

349

return key.length() + value.length();

350

};

351

352

Cache<String, String> stringCache = Caffeine.newBuilder()

353

.maximumWeight(1000)

354

.weigher(stringWeigher)

355

.build();

356

357

stringCache.put("short", "a"); // weight: 6

358

stringCache.put("medium_key", "data"); // weight: 14

359

stringCache.put("very_long_key_name", "very_long_value_content"); // weight: 43

360

```

361

362

### Complex Object Weigher

363

364

```java

365

Weigher<String, CacheableObject> objectWeigher = (key, obj) -> {

366

int baseWeight = key.length();

367

368

if (obj == null) {

369

return baseWeight;

370

}

371

372

// Calculate weight based on object characteristics

373

int objectWeight = obj.getSerializedSize();

374

375

// Add extra weight for resource-intensive objects

376

if (obj.hasLargeData()) {

377

objectWeight *= 2;

378

}

379

380

// Ensure minimum weight

381

return Math.max(baseWeight + objectWeight, 1);

382

};

383

384

Cache<String, CacheableObject> objectCache = Caffeine.newBuilder()

385

.maximumWeight(100_000)

386

.weigher(objectWeigher)

387

.recordStats()

388

.build();

389

```

390

391

### Dynamic Weigher with Validation

392

393

```java

394

Weigher<String, byte[]> byteArrayWeigher = (key, data) -> {

395

// Validate inputs

396

if (key == null || data == null) {

397

return 1; // Minimum weight for null values

398

}

399

400

// Base weight from key

401

int keyWeight = key.length();

402

403

// Data weight

404

int dataWeight = data.length;

405

406

// Apply scaling factor for very large objects

407

if (dataWeight > 10_000) {

408

dataWeight = 10_000 + (dataWeight - 10_000) / 10;

409

}

410

411

int totalWeight = keyWeight + dataWeight;

412

413

// Ensure weight is positive and reasonable

414

return Math.max(totalWeight, 1);

415

};

416

```

417

418

### Bounded Weigher for Safety

419

420

```java

421

// Potentially unsafe weigher that might return negative values

422

Weigher<String, String> unsafeWeigher = (key, value) -> {

423

// Hypothetical calculation that could go negative

424

return key.length() - value.length();

425

};

426

427

// Wrap with boundedWeigher to ensure non-negative weights

428

Weigher<String, String> safeWeigher = Weigher.boundedWeigher(unsafeWeigher);

429

430

Cache<String, String> safeCache = Caffeine.newBuilder()

431

.maximumWeight(1000)

432

.weigher(safeWeigher) // Automatically validates weights >= 0

433

.build();

434

435

// This would throw IllegalArgumentException if unsafe weigher returned negative value

436

safeCache.put("long_key", "short"); // Negative weight prevented by boundedWeigher

437

```

438

439

## Expiry Interface

440

441

The `Expiry` interface enables custom expiration policies based on entry characteristics and access patterns.

442

443

```java { .api }

444

public interface Expiry<K, V> {

445

long expireAfterCreate(K key, V value, long currentTime);

446

long expireAfterUpdate(K key, V value, long currentTime, long currentDuration);

447

long expireAfterRead(K key, V value, long currentTime, long currentDuration);

448

449

// Static factory methods

450

static <K, V> Expiry<K, V> creating(BiFunction<K, V, Duration> expireAfterCreate);

451

static <K, V> Expiry<K, V> accessing(BiFunction<K, V, Duration> expireAfterCreate,

452

TriFunction<K, V, Duration, Duration> expireAfterRead);

453

static <K, V> Expiry<K, V> writing(BiFunction<K, V, Duration> expireAfterCreate,

454

TriFunction<K, V, Duration, Duration> expireAfterUpdate);

455

}

456

```

457

458

### Dynamic Expiry Based on Value Characteristics

459

460

```java

461

Expiry<String, String> dynamicExpiry = new Expiry<String, String>() {

462

@Override

463

public long expireAfterCreate(String key, String value, long currentTime) {

464

// Different expiration based on key pattern

465

if (key.startsWith("temp_")) {

466

return Duration.ofMinutes(5).toNanos();

467

} else if (key.startsWith("session_")) {

468

return Duration.ofHours(2).toNanos();

469

} else if (value.length() > 1000) {

470

// Large values expire sooner

471

return Duration.ofMinutes(15).toNanos();

472

} else {

473

return Duration.ofHours(1).toNanos();

474

}

475

}

476

477

@Override

478

public long expireAfterUpdate(String key, String value, long currentTime, long currentDuration) {

479

// Reset to creation expiration on update

480

return expireAfterCreate(key, value, currentTime);

481

}

482

483

@Override

484

public long expireAfterRead(String key, String value, long currentTime, long currentDuration) {

485

// Extend expiration for frequently accessed items

486

if (key.startsWith("hot_")) {

487

return currentDuration + Duration.ofMinutes(10).toNanos();

488

}

489

// No change for other items

490

return currentDuration;

491

}

492

};

493

494

Cache<String, String> dynamicCache = Caffeine.newBuilder()

495

.maximumSize(1000)

496

.expireAfter(dynamicExpiry)

497

.build();

498

```

499

500

### Simplified Expiry with Factory Methods

501

502

```java

503

// Creation-only expiry

504

Expiry<String, UserSession> creationExpiry = Expiry.creating((key, session) -> {

505

// Different expiration based on user role

506

return session.isAdminUser() ? Duration.ofHours(8) : Duration.ofHours(2);

507

});

508

509

// Access-based expiry

510

Expiry<String, String> accessExpiry = Expiry.accessing(

511

(key, value) -> Duration.ofMinutes(30), // Initial expiration

512

(key, value, duration) -> Duration.ofMinutes(45) // Extended on access

513

);

514

515

// Write-based expiry

516

Expiry<String, String> writeExpiry = Expiry.writing(

517

(key, value) -> Duration.ofHours(1), // Initial expiration

518

(key, value, duration) -> Duration.ofHours(2) // Extended on update

519

);

520

```

521

522

## Ticker Interface

523

524

The `Ticker` interface provides time source abstraction for testing and custom time handling.

525

526

```java { .api }

527

@FunctionalInterface

528

public interface Ticker {

529

long read();

530

531

static Ticker systemTicker();

532

static Ticker disabledTicker();

533

}

534

```

535

536

### Custom Ticker for Testing

537

538

```java

539

public class ManualTicker implements Ticker {

540

private volatile long nanos = 0;

541

542

@Override

543

public long read() {

544

return nanos;

545

}

546

547

public void advance(Duration duration) {

548

nanos += duration.toNanos();

549

}

550

551

public void advance(long time, TimeUnit unit) {

552

nanos += unit.toNanos(time);

553

}

554

}

555

556

// Use in tests

557

ManualTicker ticker = new ManualTicker();

558

Cache<String, String> testCache = Caffeine.newBuilder()

559

.maximumSize(100)

560

.expireAfterWrite(Duration.ofMinutes(10))

561

.ticker(ticker)

562

.build();

563

564

testCache.put("key", "value");

565

566

// Simulate time passage

567

ticker.advance(Duration.ofMinutes(15));

568

testCache.cleanUp(); // Triggers expiration

569

570

String value = testCache.getIfPresent("key"); // null - expired

571

```

572

573

## Scheduler Interface

574

575

The `Scheduler` interface controls background maintenance scheduling.

576

577

```java { .api }

578

@FunctionalInterface

579

public interface Scheduler {

580

Future<?> schedule(Executor executor, Runnable command, long delay, TimeUnit unit);

581

582

static Scheduler forScheduledExecutorService(ScheduledExecutorService scheduledExecutorService);

583

static Scheduler systemScheduler();

584

static Scheduler disabledScheduler();

585

}

586

```

587

588

### Custom Scheduler Implementation

589

590

```java

591

public class CustomScheduler implements Scheduler {

592

private final ScheduledExecutorService scheduler =

593

Executors.newScheduledThreadPool(2, r -> {

594

Thread t = new Thread(r, "caffeine-maintenance");

595

t.setDaemon(true);

596

return t;

597

});

598

599

@Override

600

public Future<?> schedule(Executor executor, Runnable command, long delay, TimeUnit unit) {

601

// Add logging for maintenance operations

602

Runnable wrappedCommand = () -> {

603

System.out.println("Running cache maintenance");

604

long start = System.nanoTime();

605

try {

606

command.run();

607

} finally {

608

long duration = System.nanoTime() - start;

609

System.out.println("Maintenance completed in " +

610

TimeUnit.NANOSECONDS.toMillis(duration) + "ms");

611

}

612

};

613

614

return scheduler.schedule(wrappedCommand, delay, unit);

615

}

616

}

617

618

Cache<String, String> scheduledCache = Caffeine.newBuilder()

619

.maximumSize(1000)

620

.expireAfterAccess(Duration.ofMinutes(30))

621

.scheduler(new CustomScheduler())

622

.build();

623

```

624

625

## Interner Interface

626

627

The `Interner` interface provides object interning functionality similar to `String.intern()` for any immutable type.

628

629

```java { .api }

630

@FunctionalInterface

631

public interface Interner<E> {

632

E intern(E sample);

633

634

static <E> Interner<E> newStrongInterner();

635

static <E> Interner<E> newWeakInterner();

636

}

637

```

638

639

### Strong Interner

640

641

Strong interners retain strong references to interned instances, preventing garbage collection.

642

643

```java

644

Interner<String> strongInterner = Interner.newStrongInterner();

645

646

// Intern strings

647

String s1 = strongInterner.intern("hello");

648

String s2 = strongInterner.intern(new String("hello"));

649

String s3 = strongInterner.intern("hello");

650

651

// All return the same instance

652

assert s1 == s2;

653

assert s2 == s3;

654

assert s1 == s3;

655

656

// Memory efficient for frequently used immutable objects

657

Interner<ImmutableConfig> configInterner = Interner.newStrongInterner();

658

ImmutableConfig config1 = configInterner.intern(new ImmutableConfig("prod", "database"));

659

ImmutableConfig config2 = configInterner.intern(new ImmutableConfig("prod", "database"));

660

assert config1 == config2; // Same instance

661

```

662

663

### Weak Interner

664

665

Weak interners use weak references, allowing garbage collection when no other references exist.

666

667

```java

668

Interner<String> weakInterner = Interner.newWeakInterner();

669

670

// Intern strings with weak references

671

String s1 = weakInterner.intern("temporary");

672

String s2 = weakInterner.intern(new String("temporary"));

673

assert s1 == s2;

674

675

// After gc, instances may be collected

676

System.gc();

677

// Subsequent interns may return different instances if previous ones were collected

678

679

// Useful for reducing memory usage of temporary objects

680

Interner<RequestKey> keyInterner = Interner.newWeakInterner();

681

RequestKey key1 = keyInterner.intern(new RequestKey("user123", "action"));

682

RequestKey key2 = keyInterner.intern(new RequestKey("user123", "action"));

683

assert key1 == key2; // Same instance while both are reachable

684

```

685

686

### Performance Comparison

687

688

```java

689

public class InternerPerformanceExample {

690

public void demonstrateInterning() {

691

Interner<String> strongInterner = Interner.newStrongInterner();

692

Interner<String> weakInterner = Interner.newWeakInterner();

693

694

// Strong interner - better for frequently accessed objects

695

long start = System.nanoTime();

696

for (int i = 0; i < 10000; i++) {

697

String interned = strongInterner.intern("common_value_" + (i % 100));

698

// Process interned string

699

}

700

long strongTime = System.nanoTime() - start;

701

702

// Weak interner - better for memory-constrained scenarios

703

start = System.nanoTime();

704

for (int i = 0; i < 10000; i++) {

705

String interned = weakInterner.intern("common_value_" + (i % 100));

706

// Process interned string

707

}

708

long weakTime = System.nanoTime() - start;

709

710

System.out.println("Strong interner time: " + strongTime / 1_000_000 + "ms");

711

System.out.println("Weak interner time: " + weakTime / 1_000_000 + "ms");

712

}

713

}

714

```

715

716

### Best Practices

717

718

**When to use Strong Interner:**

719

- Frequently accessed immutable objects

720

- Objects with long lifetimes

721

- Performance-critical scenarios

722

- Small to medium number of unique instances

723

724

**When to use Weak Interner:**

725

- Large number of potentially unique instances

726

- Memory-constrained environments

727

- Temporary objects that can be garbage collected

728

- Scenarios where canonical instances may not always be needed

729

730

**Thread Safety:**

731

Both strong and weak interners are thread-safe and can be safely accessed from multiple threads concurrently.