Java vs Kotlin: Different Syntax, Same Possibilities
AM
Ashni Mehta5 min read • Published Nov 25, 2024 • Updated Nov 25, 2024
Rate this article
Ever since Java was introduced in 1995, millions of developers have used it to build applications across a variety of industries and problem spaces. Other languages have emerged, too, building on top of the solid foundation that Java has provided over the past 30 years. Apache Groovy, first released in 2007, is a Java syntax-compatible language for the JVM. Scala, intended to address some of the criticisms of Java, also can run on the JVM and is interoperable with Java.
Kotlin is the newest popular contender in the world of JVM languages. Released in 2011, it has been gaining steady popularity—first in the Android application development world, and now, in the server-side JVM space. Kotlin is interoperable with Java and mainly targets the JVM. However, it is more concise than Java, with null safety and coroutine support.
In this article, we’ll cover how to use some MongoDB features, and compare and contrast how they look and feel in Kotlin and Java.
Suppose you want to track changes to your data in MongoDB. These can be changes to a collection, database, or deployment. You can monitor real-time changes through change streams, a feature in the MongoDB server. This feature allows applications to watch for changes to data and react to them.
Change streams have been available for several MongoDB server releases, but in more recent releases, the ability to split a large change stream event (an event that exceeds 16MB) was introduced, with $changeStreamSplitLargeEvent. The ability to contain or omit a pre-image (a document that represents the version of the document before the operation) and a post-image (the same, but for after the operation) was also introduced, with ChangeStreamPreAndPostImagesOptions.
In Java, you can open a change stream on a collection and print events as they occur, as follows:
1 ChangeStreamIterable<Document> changeStream = collection.watch(); 2 changeStream.forEach(event -> System.out.println("Received a change: " + event));
The watch() method is how you can open a change stream on a MongoCollection, MongoDatabase, or MongoClient. Note that depending on the object that you call watch() on, the scope of events that the change stream listens for will change (ex: if you call watch() on a MongoDatabase, all collections in that database are monitored).
The method is similar in Kotlin, but note that in the Kotlin Coroutine driver, you can leverage the power of flows, as the watch() method will return an instance of ChangeStreamFlow, which is a Flow implementation for change streams:
1 val changeStream = collection.watch() 2 changeStream.collect { 3 println("Received a change: $it") 4 }
In MongoDB, queries can be constructed in a few ways. Before I dive into specifics, let’s start with an example. Let us assume that we want to find all patients in a
patients
collection who match the following eligibility criteria for a new healthcare trial:- Patients whose gender is “male”
- Patients who are younger than “30”
We want to return only their addresses so that we can send them a notice in the mail about this new trial.
1 collection.find({ "gender": "male", "age" : { "$lt": 30 }}, { "_id": 0, "address": 1 })
The results of the above query would be a set of documents containing only the address field (the _id field has been suppressed by being projected out). If we were to use the Java driver, without builders, our query would resemble the following:
1 Bson filter = new Document().append("gender", "male").append("age", new Document().append("$lt", 30)); 2 Bson projection = new Document().append("_id", 0).append("address", 1); 3 4 collection.find(filter).projection(projection);
The above would yield the addresses for patients who match the earlier criteria. However, the driver also provides methods to simplify the process of using CRUD operations and the Aggregation API to build queries more efficiently. These methods are called builders.
If we were to use builders, instead of the above code, our code in Java might resemble the following:
1 import static com.mongodb.client.model.Filters.*; 2 import static com.mongodb.client.model.Projections.*; 3 4 Bson filter = and(eq("gender", "male"), lt("age", 30)); 5 Bson projection = fields(excludeId(), include("address")); 6 7 collection.find(filter).projection(projection);
There are several builders that are available in the Java driver today:
- Filters, for building query filters
- Projections, for building projects
- Sorts, for building sort criteria
- Updates, for building updates
- Aggregates, for building aggregation pipelines
- Indexes, for creating index keys
In the above example, we use both the filters and projections. Filters are operations MongoDB uses to limit results to what you want to see, and projections allow you to specify which fields to include and exclude in query results. Projections are particularly useful when you want to limit the amount of data MongoDB is sending to applications, to optimize performance or reduce storage costs.
The same builders are available in Kotlin and can be used with slightly different syntax. If we consider the non-builder case in Kotlin, our code might look like the following:
1 val filter = Document().append("gender", "male").append("age", Document().append("\$lt", 30)) 2 val projection = Document().append("_id", 0).append("address", 1) 3 val results = collection.find<Results>(filter).projection(projection)
Builders simplify this quite a bit, with the resulting code resembling the following:
1 val filter = Filters.and(Filters.eq(User::gender.name, "male"), Filters.lt(User::age.name, 30)) 2 val projection = Projections.fields(Projections.excludeId(), Projections.include("address")) 3 val results = collection.find<Results>(filter).projection(projection)
MongoDB supports encryption in several ways:
(1) In transit through TLS/SSL, to ensure that MongoDB network traffic is only readable by the intended client
(2) At rest, to help ensure compliance with security and privacy standards like HIPAA, PCI-DSS, and FERPA
(3) In-use, to encrypt data when transmitted, stored, and processed
Queryable Encryption is a form of in-use encryption that allows you to encrypt sensitive data from the client-side, store sensitive fields as fully randomized encrypted data on the server-side, and run expressive queries on the encrypted data. It is an industry-first, fast, searchable encryption scheme that allows you to perform expressive queries on encrypted fields without the need for deterministic encryption or hardware enclaves. Although Queryable Encryption was launched last year, with support for equality queries, with MongoDB 8.0 comes the ability to run range queries, as well. Range queries can be used to filter documents with specific value ranges, like dates or numerical intervals.
To use the new range support, you’ll need to first: \
- Optional: If you’re using explicit encryption (as opposed to automatic encryption), you will also need to create a Data Encryption Key for your encrypted field(s).
Once you have your application, you’ll need to:
- Specify which fields you’d like to encrypt by adding them to the encryption schema, with the
queries
property. Similar to specifying aqueryType
of “equality
” for equality queries, for range queries, you’d want to specify aqueryType
of “range
”. - Instantiate ClientEncryption to access the API for encryption helper methods (you can use ClientEncryptions#create to do so).
- Create the collection (note that it is recommended to explicitly create the collection so that an index is created on the encrypted fields).
- Insert at least one document with encrypted fields.
For a more in-depth guide to the above steps, we have code snippets throughout our documentation.
Once a field has been designated as encrypted, and at least one document has been inserted into your collection with one of those encrypted fields, you can query to find matching documents. In Java, a range query to find a patient whose bill amount is between 1000 and 2000 might resemble the following:
1 Document filter = new Document("patientRecord.billAmount", new Document("$gt", 1000).append("$lt", 2000)); 2 Patient findResult = collection.find(filter).first();
Kotlin is not so different, syntax-wise, with the resulting range query resembling the following:
1 val filter = Document("patientRecord.billAmount", Document("\$gt", 1000).append("\$lt", 2000)) 2 val findResult: Patient? = collection.find(filter).first()
To explore Range Queries (and Queryable Encryption) in greater detail, check out our tutorial, Java Meets Queryable Encryption! It’ll take you step by step through the process of setting up a project, configuring automatic encryption, the encryption classes, and more.
In the above article, we touched on key features of MongoDB and how to use them in two different, but complementary, languages. Java and Kotlin are two distinct languages that target the JVM, but with MongoDB support for both, you can rest assured that you can interact with your data in the language of your choice. Plus, because Java and Kotlin are interoperable, you can leverage the power of the long-standing Java ecosystem and the libraries and utilities within it, to build new Kotlin applications.
Top Comments in Forums
There are no comments on this article yet.