惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

V
Visual Studio Blog
MongoDB | Blog
MongoDB | Blog
Engineering at Meta
Engineering at Meta
云风的 BLOG
云风的 BLOG
Microsoft Azure Blog
Microsoft Azure Blog
B
Blog RSS Feed
T
The Exploit Database - CXSecurity.com
P
Privacy & Cybersecurity Law Blog
Know Your Adversary
Know Your Adversary
月光博客
月光博客
I
InfoQ
阮一峰的网络日志
阮一峰的网络日志
NISL@THU
NISL@THU
爱范儿
爱范儿
S
Securelist
博客园 - 叶小钗
C
CERT Recently Published Vulnerability Notes
Recorded Future
Recorded Future
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
aimingoo的专栏
aimingoo的专栏
D
DataBreaches.Net
G
GRAHAM CLULEY
P
Proofpoint News Feed
A
About on SuperTechFans
Google DeepMind News
Google DeepMind News
C
Cyber Attacks, Cyber Crime and Cyber Security
Threat Intelligence Blog | Flashpoint
Threat Intelligence Blog | Flashpoint
T
Tor Project blog
Stack Overflow Blog
Stack Overflow Blog
T
Threat Research - Cisco Blogs
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
T
Tailwind CSS Blog
有赞技术团队
有赞技术团队
Hugging Face - Blog
Hugging Face - Blog
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
Recent Announcements
Recent Announcements
P
Proofpoint News Feed
The GitHub Blog
The GitHub Blog
The Cloudflare Blog
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
Last Week in AI
Last Week in AI
Y
Y Combinator Blog
Jina AI
Jina AI
大猫的无限游戏
大猫的无限游戏
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
罗磊的独立博客
博客园 - 【当耐特】
H
Help Net Security
F
Fortinet All Blogs
T
The Blog of Author Tim Ferriss

DEV Community

Authentication Security Deep Dive: From Brute Force to Salted Hashing (With Java Examples) Why AI Systems Don’t Fail — They Drift Spilling beans for how i learn for exam😁"Reinforcement Learning Cheat Sheet" I Replaced Chrome with Safari for AI Browser Automation. Here's What Broke (and What Finally Worked) How Python Borrows Other People's Work The $40 Architecture: Processing 1 Billion API Requests with 99.99% Uptime Vibe Coding: A Workflow Guide (From Zero to SaaS) Most webhook security guides protect the wrong side. The scary part is delivery. Headless CMS for TanStack Start: Build a Blog with Cosmic EU Age Verification App "Hacked in 2 Minutes" — What Actually Happened Comfy Cloud’s delete function does not actually remove files Running AI Models on GPU Cloud Servers: A Beginner Guide Event-driven media intelligence with AWS Step Functions and Bedrock I scored 500 AI prompts across 8 quality dimensions — here's what broke How to Call Google Gemini API from Next.js (Free Tier, No Backend Needed) The Portal Protocol: Reclaiming Human Connection in the Age of AI How to Fix Your Team's Scattered Knowledge Problem With a Self-Hosted Forum Intro to tc Cloud Functors: A Graph-First Mental Model for the Modern Cloud Designing Multi-Tenant Backends With Both Ownership and Team Access I Built a Neumorphic CSS Library with 77+ Components — Here's What I Learned PostgreSQL Performance Optimization: Why Connection Pooling Is Critical at Scale Cómo construí un SaaS multi-rubro para gestionar expensas en Argentina con FastAPI + Vue 3 🚀 I Built an Ethical Hacking Scanner Tool – Open Source Project I Replaced /usage and /context in Claude Code With a Single Statusline A Pythonic Way to Handle Emails (IMAP/SMTP) with Auto-Discovery and AI-Ready Design I Collected 8.9 Million Polymarket Price Points — Here's What I Found About How Markets Really Move EcoTrack AI — Carbon Footprint Tracker & Dashboard Everyone's Using AI. No One Agrees How. 5 self-hosted ebook managers worth trying in 2026 Building Your First AI Agent with LangChain: From Chatbot to Autonomous Assistant Common SOC 2 Failures (Real World) Stop Vibe-Checking Your AI App: A Practical Guide to Evals How to Use SonarQube and SonarScanner Locally to Level Up Your Code Quality Your Next To-Do App Is Dead — I Replaced Mine with an OpenClaw AI Sign a Nostr event in 60 lines of Python using coincurve — no nostr-sdk, no nbxplorer, no rust toolchain ITGC Audit Explained Like You’re in Big 4 Patch Tuesday abril 2026: Microsoft parcha 163 vulnerabilidades y un zero-day en SharePoint Stop scraping everything: a better way to track competitor price changes Listing on MCPize + the Official MCP Registry while routing payments OUTSIDE the marketplace — how I kept 100% of my x402 revenue Building an AI-Powered Risk Intelligence System Using Serverless Architecture Why We Ripped Function Overloading Out of Our AI Toolchain Testing AI-Generated Code: How to Actually Know If It Works SaaS Churn Is Killing Your Business. Here Is What to Do About It (Without a Support Team) The Speed of AI Is No Longer Linear - And Self-Improving Models Are Why How to Implement RBAC for MCP Tools: A Practical Guide for Engineering Teams From Standard Quote to Persuasive Proposal: AI Automation for Arborists I built a CLI that scaffolds complete multi-tenant SaaS apps Axios CVE-2025–62718: The Silent SSRF Bug That Could Be Hiding in Your Node.js App Right Now The dashboard that ended our friendship Data Pipelines Explained Simply (and How to Build Them with Python) The Hidden Cost of AI Systems Nobody Talks About. undefined vs undeclared, and how typeof behaves Switching from file-based jobs to NATS/Kafka in Rust without changing code io_uring Adventures: Rust Servers That Love Syscalls Why Agentic AI is Killing the Traditional Database The POUR principles of web accessibility for developers and designers Quantum Neural Network 3D — A Deep Dive into Interactive WebGL Visualization How To Install Caveman In Codex On macOS And Windows Automation Pipeline Reliability: Why Your Workflow Breaks When Nobody Is Watching I Built an 'Open World' AI Coding Agent — It Works From ANY Folder From Freelancing to Product: A Tech Service Company's SaaS Transformation China's AI Giants: Adding Tencent Hunyuan & ByteDance Doubao to AI University (74 Providers) On the Vibe Coders and Their Lies clerk: Auto-Summarize Your Claude Code Sessions AI Weekly — 2026/04/10–04/17 | The Model Lockdown Is Here, but the Toolchain Is the Real Battleground AI 週報 — 2026/04/10–2026/04/17 模型封鎖潮來了,但工具鏈才是真戰場 Maybe this is how Open-Source apps are born... 🚀 Fine-Tune LLMs with LoRA and QLoRA: 2026 Guide tRPC v11 + Next.js App Router: End-to-End Type Safety Without the Boilerplate ShadCN UI in 2026: Why I Stopped Installing Component Libraries and Started Owning My Components SaaS Billing in React Server Components: Stripe + Supabase Without a Single `useEffect` Join our DEV Weekend Challenge — $1,000 in Prizes Across TEN winners! Submissions Due April 20 at 6:59 AM UTC. Implementing FSRS Spaced Repetition in Flutter + Supabase — Adding Memory Science to an AI Learning App "I Texted My Localhost From the Train — Claude Code Fixed the Bug Before I Got Home" I Built a Sales Prep AI and It Went Deeper Than Expected Design to Code #2: One JSON, Eleven Outputs Solving the 100M-Row Problem: A Summary Table Pattern for High-Volume Push Notification Logs Flutter Web With Wasm: What Actually Changes For Developers I Built 50 Royalty-Free Soundtracks for My Side Project in a Weekend Using AI Music Generation The Vibe Coding Security Checklist: 7 Things to Check Before You Ship Stop Letting Googlebot Guess Fix Your React App's SEO Right Desconstruindo o Streaming do LinkedIn: Como Criar um Engine de Extração de Vídeo de Alta Performance com HLS e FFmpeg (EDA Part-1) EDA (Exploratory Data Analysis) Explained With Real Life — Why Looking at Your Data Is the Most Important Step in Machine Learning Brand Relationship Management at Scale: Our 4-Touch Outreach System for 200+ Brands Why String.fromEnvironment() Might Return an Empty String in Dart JGuardrails 1.0.0 — Hardening Java LLM Apps Against Jailbreaks, Toxicity, and Prompt Injection Plan and Schedule a Full Week of Threads Content From One Claude Conversation Coding Cat Oran Ep3, Five Tables Changed Everything Updated: BFF Pattern I'm done watching freelancers get buried by 200 proposals. So I'm building the alternative. This is my first post BFS Algorithm in Java Step by Step Tutorial with Examples Tracking LLM Pricing Monthly: An Open Dataset for 22 AI Models How We Measure Content ROI on a Comparison Site: Revenue Attribution Without Perfect Data Introducing Nova AI Ops: The AI-Native Operating System for SRE Teams I built a free desktop video downloader for Windows — Grabbit How Talkie OCR Helps Vision-Impaired & Dyslexic Users Read the World Around Them VRCFaceTracking安装和iPhone面捕配置教程,有bug Even CrowdStrike Can't See Your Agents The Automation Gold Rush: What n8n Workflows and Claude Are Opening Up for Developers Right Now
5 Custom Java Collector Patterns That Replace Complex Stream Reduction Code
Nithin Bhara · 2026-05-10 · via DEV Community

As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!

I remember the first time I hit the limits of Java’s Collectors.toList() and Collectors.groupingBy(). I was building a real‑time dashboard for a trading system. The standard collectors could not handle my custom statistics, my multi‑level grouping, or my thread‑safe accumulations. That day I learned to write my own collectors. And it changed the way I look at streams.

Let’s walk through five patterns for writing custom collectors that encapsulate aggregation logic into reusable, composable components. I will keep this as simple as I can, as if I am explaining it to my younger self.


A collector in Java is just a box with four jobs. First, it needs a factory that creates an empty container – that is the supplier. Then it needs a way to drop one element into that container – the accumulator. When streams run in parallel, it needs a method to merge two containers into one – the combiner. And finally, a finisher that transforms the container into the final result you want.

The Java Collector interface captures these four functions plus a set of characteristics that tell the stream how to behave. If you understand these pieces, you can build a collector for almost any aggregation.

I will show you a simple statistics collector that calculates count, sum, minimum, and maximum from a stream of numbers. This pattern is the foundation for everything else.

public class StatisticsCollector<T> implements Collector<T, StatisticsCollector.Accumulator, Statistics> {

    static class Accumulator {
        long count;
        double sum;
        double min = Double.MAX_VALUE;
        double max = Double.MIN_VALUE;
    }

    @Override
    public Supplier<Accumulator> supplier() {
        return Accumulator::new;
    }

    @Override
    public BiConsumer<Accumulator, T> accumulator() {
        return (acc, value) -> {
            double num = ((Number) value).doubleValue();
            acc.count++;
            acc.sum += num;
            acc.min = Math.min(acc.min, num);
            acc.max = Math.max(acc.max, num);
        };
    }

    @Override
    public BinaryOperator<Accumulator> combiner() {
        return (left, right) -> {
            left.count += right.count;
            left.sum += right.sum;
            left.min = Math.min(left.min, right.min);
            left.max = Math.max(left.max, right.max);
            return left;
        };
    }

    @Override
    public Function<Accumulator, Statistics> finisher() {
        return acc -> new Statistics(acc.count, acc.sum, acc.min, acc.max);
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Set.of(Characteristics.CONCURRENT, Characteristics.UNORDERED);
    }

    public static <T extends Number> Collector<T, ?, Statistics> toStatistics() {
        return new StatisticsCollector<>();
    }
}

Enter fullscreen mode Exit fullscreen mode

Notice the CONCURRENT and UNORDERED characteristics. CONCURRENT tells the stream that the accumulator can run safely from multiple threads on the same container – but only if the stream is also unordered. UNORDERED means the order of elements does not matter for the result. Together they allow the stream to skip merging and use a single container for parallel processing. This can be a huge performance win.

But you must be careful. If your accumulator modifies shared state without proper synchronization, you will get corrupted results. In this example, the operations are simple additions and comparisons, which are safe because the accumulator runs on one container at a time (the supplier creates a new container per thread, but with CONCURRENT the supplier might be called only once – check the Java documentation). For truly concurrent collectors, I use atomic classes like LongAdder and AtomicReference. Let me show you that next.


Thread‑Safe Collectors for Parallel Streams

When I write collectors for high‑throughput systems, I use concurrent building blocks. The pattern stays the same, but the accumulator uses thread‑safe objects.

public class ConcurrentStatisticsCollector<T extends Number>
        implements Collector<T, ConcurrentStatisticsCollector.ConcurrentAccumulator, Statistics> {

    static class ConcurrentAccumulator {
        final LongAdder count = new LongAdder();
        final DoubleAdder sum = new DoubleAdder();
        final AtomicReference<Double> min = new AtomicReference<>(Double.MAX_VALUE);
        final AtomicReference<Double> max = new AtomicReference<>(Double.MIN_VALUE);
    }

    @Override
    public Supplier<ConcurrentAccumulator> supplier() {
        return ConcurrentAccumulator::new;
    }

    @Override
    public BiConsumer<ConcurrentAccumulator, T> accumulator() {
        return (acc, value) -> {
            double num = value.doubleValue();
            acc.count.increment();
            acc.sum.add(num);
            acc.min.updateAndGet(current -> Math.min(current, num));
            acc.max.updateAndGet(current -> Math.max(current, num));
        };
    }

    @Override
    public BinaryOperator<ConcurrentAccumulator> combiner() {
        return (left, right) -> {
            left.count.add(right.count.sum());
            left.sum.add(right.sum.sum());
            left.min.updateAndGet(current -> Math.min(current, right.min.get()));
            left.max.updateAndGet(current -> Math.max(current, right.max.get()));
            return left;
        };
    }

    @Override
    public Function<ConcurrentAccumulator, Statistics> finisher() {
        return acc -> new Statistics(
            acc.count.sum(),
            acc.sum.sum(),
            acc.min.get(),
            acc.max.get()
        );
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Set.of(Characteristics.CONCURRENT, Characteristics.UNORDERED);
    }
}

Enter fullscreen mode Exit fullscreen mode

The combiner still merges two accumulators, but it does so by adding the values from both. This is safe because each accumulator was built in its own thread. The CONCURRENT characteristic allows the stream to avoid merging if it knows only one thread will ever call the accumulator – but with atomic fields you are prepared for either case.

I use this pattern when I process millions of events per second. The overhead of atomic operations is small compared to the cost of merging intermediate results.


Multi‑Level Grouping

Grouping by one key is easy. Grouping by two keys and keeping a list of values inside each leaf – that is a custom collector. I needed this when I had to organise sales data by region and then by product category, while preserving the order of individual transactions.

Here is a cascading grouping collector that builds a nested map.

public class CascadingGroupingCollector<T, K1, K2, V>
        implements Collector<T, Map<K1, Map<K2, List<V>>>, Map<K1, Map<K2, List<V>>>> {

    private final Function<? super T, ? extends K1> classifier1;
    private final Function<? super T, ? extends K2> classifier2;

    public CascadingGroupingCollector(
            Function<? super T, ? extends K1> classifier1,
            Function<? super T, ? extends K2> classifier2) {
        this.classifier1 = classifier1;
        this.classifier2 = classifier2;
    }

    @Override
    public Supplier<Map<K1, Map<K2, List<V>>>> supplier() {
        return HashMap::new;
    }

    @Override
    public BiConsumer<Map<K1, Map<K2, List<V>>>, T> accumulator() {
        return (map, item) -> {
            K1 key1 = classifier1.apply(item);
            K2 key2 = classifier2.apply(item);
            map.computeIfAbsent(key1, k -> new HashMap<>())
               .computeIfAbsent(key2, k -> new ArrayList<>())
               .add((V) item);
        };
    }

    @Override
    public BinaryOperator<Map<K1, Map<K2, List<V>>>> combiner() {
        return (left, right) -> {
            right.forEach((key1, innerMap) -> {
                left.merge(key1, innerMap, (existing, incoming) -> {
                    incoming.forEach((key2, list) ->
                        existing.merge(key2, list, (l1, l2) -> {
                            l1.addAll(l2);
                            return l1;
                        }));
                    return existing;
                });
            });
            return left;
        };
    }

    @Override
    public Function<Map<K1, Map<K2, List<V>>>, Map<K1, Map<K2, List<V>>>> finisher() {
        return Function.identity();
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Set.of(Characteristics.UNORDERED);
    }
}

Enter fullscreen mode Exit fullscreen mode

Notice that the finisher returns the map as‑is. That is because the intermediate container is also the final result. This works only if the characteristics do not include IDENTITY_FINISH. If you mark a collector as IDENTITY_FINISH, the stream can skip calling the finisher entirely. Here I deliberately left it out to avoid confusion. In real code, you could add IDENTITY_FINISH and remove the finisher override.

The combiner merges nested maps recursively. It uses merge to handle both levels of keys. This pattern scales to three or more levels – just add another layer of maps and another merge loop.


Top‑N Collector

Sometimes you do not need all the elements – only the ten largest, or the five most recent. The standard toList() gives you everything. A custom top‑N collector keeps only the best items using a bounded TreeSet.

public class TopNCollector<T> implements Collector<T, TreeSet<T>, List<T>> {

    private final Comparator<? super T> comparator;
    private final int n;

    public TopNCollector(Comparator<? super T> comparator, int n) {
        this.comparator = comparator;
        this.n = n;
    }

    @Override
    public Supplier<TreeSet<T>> supplier() {
        return () -> new TreeSet<>(comparator);
    }

    @Override
    public BiConsumer<TreeSet<T>, T> accumulator() {
        return (set, item) -> {
            set.add(item);
            if (set.size() > n) {
                set.pollLast();
            }
        };
    }

    @Override
    public BinaryOperator<TreeSet<T>> combiner() {
        return (left, right) -> {
            for (T item : right) {
                left.add(item);
                if (left.size() > n) {
                    left.pollLast();
                }
            }
            return left;
        };
    }

    @Override
    public Function<TreeSet<T>, List<T>> finisher() {
        return set -> {
            List<T> list = new ArrayList<>(set);
            Collections.reverse(list);
            return list;
        };
    }

    @Override
    public Set<Characteristics> characteristics() {
        return Set.of(Characteristics.UNORDERED);
    }

    public static <T extends Comparable<? super T>> Collector<T, ?, List<T>> topN(int n) {
        return new TopNCollector<>(Comparator.naturalOrder(), n);
    }

    public static <T> Collector<T, ?, List<T>> topN(Comparator<? super T> comparator, int n) {
        return new TopNCollector<>(comparator, n);
    }
}

Enter fullscreen mode Exit fullscreen mode

The magic happens in the accumulator. It adds the element, then immediately removes the worst one if the set exceeds the limit. This keeps memory constant regardless of stream size. The combiner does the same when merging two sets from parallel threads.

I used this collector when I needed to show the top‑10 trending products from a live feed. Instead of storing every product, I kept only the ten with the highest score. The stream never had to hold more than ten plus a few duplicates.

The finisher converts the TreeSet into a list in descending order (because I reversed it). If you want ascending order, skip the reverse.


Composing Collectors with Teeing

Java 12 introduced Collectors.teeing to send stream elements to two collectors and merge their results. But its capacity is limited to two. If you need three or four aggregations from a single pass, you can write your own teeing collector that handles any number of branches.

Here is a simplified version that works for two downstream collectors. You can extend it to more by using a list of collectors and a merger that receives a list of results.

public class TeeingCollector<T, R1, R2, R> implements Collector<T, TeeingCollector.Accumulator<R1, R2>, R> {

    static class Accumulator<R1, R2> {
        R1 leftResult;
        R2 rightResult;
    }

    private final Collector<T, ?, R1> left;
    private final Collector<T, ?, R2> right;
    private final BiFunction<? super R1, ? super R2, R> merger;

    public TeeingCollector(
            Collector<T, ?, R1> left,
            Collector<T, ?, R2> right,
            BiFunction<? super R1, ? super R2, R> merger) {
        this.left = left;
        this.right = right;
        this.merger = merger;
    }

    @Override
    @SuppressWarnings("unchecked")
    public Supplier<Accumulator<R1, R2>> supplier() {
        return () -> {
            Accumulator<R1, R2> acc = new Accumulator<>();
            acc.leftResult = ((Supplier<R1>) left.supplier()).get();
            acc.rightResult = ((Supplier<R2>) right.supplier()).get();
            return acc;
        };
    }

    // ... full implementation would include accumulator, combiner, finisher
}

Enter fullscreen mode Exit fullscreen mode

The code above is a skeleton. A complete implementation would store the intermediate containers from each downstream collector. The accumulator would apply both downstream accumulators to each element. The combiner would merge both intermediate containers separately. The finisher would apply the downstream finishers and then the merger.

I built a three‑branch teeing collector for a report that needed counts, sums, and averages from the same stream. Instead of iterating three times, I wrote one collector that ran all three in parallel.


Personal Touches and Practical Advice

When I started writing custom collectors, I made two mistakes. First, I forgot to set the characteristics correctly. If your collector does not have UNORDERED but your stream is ordered, the stream may create more intermediate containers than necessary. Second, I wrote accumulators that were not thread‑safe for parallel streams, leading to rare bugs that only appeared under heavy load.

Test your collectors with both sequential and parallel streams. Use parallelStream() and verify that the result matches. I usually create a small test dataset, then compare the output of a known correct method with the custom collector.

Do not be afraid to inline small collectors with Collector.of(). For quick one‑offs, the factory method is easier than writing a full class. But for reusable logic, writing a dedicated class makes it testable and readable.

Collector<Transaction, ?, Map<Integer, List<Transaction>>> byAmountBucket =
    Collector.of(
        HashMap::new,
        (map, tx) -> map.computeIfAbsent(tx.amount() / 1000, k -> new ArrayList<>()).add(tx),
        (left, right) -> {
            right.forEach((k, v) -> left.merge(k, v, (l1, l2) -> { l1.addAll(l2); return l1; }));
            return left;
        }
    );

Enter fullscreen mode Exit fullscreen mode

This inline collector groups transactions into buckets of thousands. No new class needed.


Summary of the Five Patterns

  1. Statistics Collector – Accumulates multiple numeric metrics in one pass.
  2. Concurrent Collector – Uses atomic classes for safe parallel accumulation.
  3. Cascading Grouping Collector – Builds nested maps with multiple keys.
  4. Top‑N Collector – Keeps only a bounded number of elements using a sorted set.
  5. Teeing Collector – Feeds the stream to multiple downstream collectors and merges.

Each pattern follows the same contract: supplier, accumulator, combiner, finisher. Once you internalise that, you can design collectors for any aggregation problem.

Custom collectors have saved me countless hours of manual iteration and temporary collections. They turn messy loops into clean, declarative pipeline code. And because they implement the Collector interface, they work automatically with parallel streams, giving you free performance gains when your data grows.

The next time you find yourself writing a for loop that reduces a stream into a complex structure, stop and ask: can I package this logic into a collector? If the answer is yes, you will end up with code that is easier to read, easier to test, and easier to reuse. I promise you, it is worth the effort.

📘 Checkout my latest ebook for free on my channel!

Be sure to like, share, comment, and subscribe to the channel!


101 Books

101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.

Check out our book Golang Clean Code available on Amazon.

Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!

Our Creations

Be sure to check out our creations:

Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | Java Elite Dev | Golang Elite Dev | Python Elite Dev | JS Elite Dev | JS Schools


We are on Medium

Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva