From October 28 to 29, Joker 2019 was held in St. Petersburg - the largest and most hardcore conference in the vastness of Russia dedicated to Java development. The event was held for the seventh time and, as always, broke the record for attendance, this time the event attracted more than 2000 specialists.
Classmates traditionally take part in Joker as event partners. This year, at our booth, one could try to cope with the famous "unsolvable" tasks from leading OK.RU engineers. Conference participants who answered the questions correctly received prizes.
In fairness, I must say that out of 1,000 leaflets with the tasks that we handed out, less than 100 were returned back. The best was the solution, which scored 4.5 points out of 5 possible.
We publish tasks and their solutions so that you can test your strengths.
1. Heroic enum
In the source code of one little-known game, such a code was discovered. What is the bad implementation of
Group.of
, and how to fix it?
enum Group { Few(1, 4), Several(5, 9), Pack(10, 19), Lots(20, 49), Swarm(50, Integer.MAX_VALUE); Group(int min, int max) { ... } public static Group of(int count) { for (Group group : Group.values()) { if (count >= group.min && count <= group.max) { return group; } } throw new IllegalArgumentException(); } }
Decision
If you donβt talk about coding styles, this fragment has an objective drawback - a potential performance problem. Although linear search often turns out to be a bottleneck, in this case it is not the point, because this enum has only five elements. And what really can negatively affect performance is the excessive allocation of memory when calling
. The problem is that the
method of enum every time returns a new copy of the array, and HotSpot is not yet able to optimize it. A simple solution is to make your own copy of the
array and iterate over it:
Group.values()
. The problem is that the
values()
method of enum every time returns a new copy of the array, and HotSpot is not yet able to optimize it. A simple solution is to make your own copy of the
values()
array and iterate over it:
private static final Group[] ALL_GROUPS = Group.values(); public static Group of(int count) { for (Group group : ALL_GROUPS) { .... }
2. Dreams
Java 13 has already been released, and Nikolai still only comprehends streams. Indicate errors in the method that calculates the difference between the maximum and minimum stream elements.
int getDiameter(Stream<Integer> stream) { int min = stream.min(Integer::compare).get(); int max = stream.max(Integer::compare).get(); return max - min; }
Decision
Streams in Java are usually one-time: calling the second terminal operation (in this case,
) will fail:
In addition,
and
return
, the
operation on which will throw a
for an empty stream. Therefore, it is more correct to check
before calling
or use other
methods: orElse , orElseThrow , etc.
Finally, the fact that the difference between the two
can no longer fit into the
will not escape the careful developer, and it would be worth changing the type of the return value to
.
max
) will fail:
java.lang.IllegalStateException: stream has already been operated upon or closed
In addition,
min
and
max
return
Optional
, the
get()
operation on which will throw a
NoSuchElementException
for an empty stream. Therefore, it is more correct to check
isPresent()
before calling
get()
or use other
Optional
methods: orElse , orElseThrow , etc.
Finally, the fact that the difference between the two
int
can no longer fit into the
int
will not escape the careful developer, and it would be worth changing the type of the return value to
long
.
3. Secure buffer
ByteBuffer
Java synchronization primitive can make
put
and
get
operations thread safe on a generic
ByteBuffer
?
final ByteBuffer buf = ByteBuffer.allocate(SIZE); int get(int offset) { return buf.get(offset); } void put(int offset, int value) { buf.putInt(offset, value); }
Choose the most effective option if you know that there are a lot of threads, and get is executed much more often than put.
- synchronized
- Reentrantlock
- ReentrantReadWriteLock
- Stampedlock
- Semaphore
- Reading and writing int in Java is always atomic
Decision
begs for reader and writer
, and often this will be an effective solution. But note that in this case, the get and put operations are very simple - the probability that a competitive put can interfere with get is small, moreover, put conditions are less likely to occur with put operations. So, you can apply the optimistic locking mechanism that StampedLock provides.
will be more efficient than
due to the fact that in case of an optimistic fast path success, shared variables are not updated at all, while
performs at least one CAS at best.
ReentrantReadWriteLock
begs for reader and writer
ReentrantReadWriteLock
, and often this will be an effective solution. But note that in this case, the get and put operations are very simple - the probability that a competitive put can interfere with get is small, moreover, put conditions are less likely to occur with put operations. So, you can apply the optimistic locking mechanism that StampedLock provides.
StampedLock
will be more efficient than
ReentrantReadWriteLock
due to the fact that in case of an optimistic fast path success, shared variables are not updated at all, while
ReentrantReadWriteLock
performs at least one CAS at best.
4. Gifts
Ilya is developing a showcase of gifts on a social network. Help him write the
add
method for a structure that holds no more than N of the newest gifts. A gift should not be added if it is already present, or if it is older than the rest of N.
interface Present { long getId(); Date getCreated(); } void add(Present p) { // Implement me }
Decision
or
naturally suitable as a data structure in order to effectively add gifts and delete the oldest no worse than for O (log N). All the trick is only in the comparator: it is not enough to compare gifts only with
, because the creation date does not have to be unique. Therefore, you need to compare first by
, then by
. Such a comparator will ensure both the uniqueness of the elements and the ordering by date.
It remains a small matter: when adding a gift, check that the size does not exceed N, and if necessary, delete the first, oldest, element of the collection.
TreeSet
or
PriorityQueue
naturally suitable as a data structure in order to effectively add gifts and delete the oldest no worse than for O (log N). All the trick is only in the comparator: it is not enough to compare gifts only with
getCreated()
, because the creation date does not have to be unique. Therefore, you need to compare first by
getCreated()
, then by
getId()
. Such a comparator will ensure both the uniqueness of the elements and the ordering by date.
TreeSet<Present> tree = new TreeSet<>( Comparator.comparing(Present::getCreated) .thenComparing(Present::getId));
It remains a small matter: when adding a gift, check that the size does not exceed N, and if necessary, delete the first, oldest, element of the collection.
void add(Present p) { if (tree.add(p) && tree.size() > N) { tree.pollFirst(); } }
5. You will not wait
Why Julia will never wait for the end of this program?
var executor = Executors.newFixedThreadPool(4); for (File f : File.listRoots()) { executor.submit(() -> f.delete()); } executor.awaitTermination(2, TimeUnit.HOURS);
Decision
The awaitTermination documentation suggests that shutdown request should precede execution. It's simple: Julia forgot to call executor.shutdown () .