1

I am experiencing a very strange problem. I am sending an object that has two lists (in my case, synchronized ArrayLists) from a server to a client. One is a list of users connected to the server, and the other is a list of lobbies that exist on the server. Every lobby also has its own list of users that are connected to it. After creating the object that encapsulates all of these lists and their data on the server side, I walk through the user list and print out each user's username as well as the ID of the lobby they are currently in. I then send the object using an ObjectOutputStream over a socket.

On the client side I receive the object using an ObjectInputStream, cast it to my encapsulating object type, and then again walk through the user list, printing out each user's username and the ID of the lobby that they are in.

Here is where my problem occurs. Every user's username (and other data) is printed correctly, except the ID of the lobby that they are in. This is somehow null, even though the printing on the server side printed the IDs correctly.

Usernames are Strings, IDs are UUIDs, and users and lobbies each have their own respective class that I created.

I originally thought this could be some issue with passing by reference vs by value, or an issue with UUIDs, but even after changing IDs to be just a normal int, this still happens.

I then thought it could be my list structure. At the time I was using a ConcurrentHashMap to store users and lobbies. I changed it to a synchronized list, but the same thing kept happening.

Note that when a new user connects, they receive a correct copy of the lists. But any lobbies that they or others join/leave are again not reflected correctly. Everything on the server side tracks and prints correctly though.

This is a relatively big project for me, and everything is rather connected, so I am unsure what code examples or output I should include. Please feel free to suggest anything that should be included/specified in my question, and I will add it. I'm still new to StackOverflow.

Any and all help is greatly appreciated!

EDIT: Here is a minimal reproducible example, sorry that it is so long.

Server.java:

import java.io.IOException;
import java.net.ServerSocket;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class Server implements Runnable {
  private final ServerSocket serverSocket;
  private ConcurrentHashMap<UUID, User> users;

  public Server(int port) throws IOException {
    serverSocket = new ServerSocket(port);
    users = new ConcurrentHashMap<>();
  }

  public ConcurrentHashMap<UUID, User> getUsers() {
    return users;
  }

  public void addUser(User user) {
    users.put(user.getId(), user);
  }

  private void setUserCurrentLobbyId(User user, UUID lobbyId) {
    users.get(user.getId()).setCurrentLobbyId(lobbyId);
  }

  @Override
  public void run() {
    while (true) {
      try {
        (new Thread(new ClientHandler(this, serverSocket.accept()))).start();
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    }
  }

  public static void main(String[] args) throws IOException, InterruptedException {
    Server server = new Server(5050);
    (new Thread(server)).start();
    User user1 = new User("user1");
    server.addUser(user1);
    Thread.sleep(5000);
    server.setUserCurrentLobbyId(user1, UUID.randomUUID());
  }
}

ClientHandler.java:

import java.io.IOException;
import java.io.ObjectOutputStream;
import java.net.Socket;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public class ClientHandler implements Runnable {
  private final Server server;
  private final Socket socket;
  private ConcurrentHashMap<UUID, User> users;

  public ClientHandler(Server server, Socket socket) {
    this.server = server;
    this.socket = socket;
    users = new ConcurrentHashMap<>();
  }

  @Override
  public void run() {
    ObjectOutputStream out;
    try {
      out = new ObjectOutputStream(socket.getOutputStream());
    } catch (IOException e) {
      throw new RuntimeException(e);
    }

    while (true) {
      updateMaps();
      Response response = new Response(users);

      System.out.println("Sent user list:");
      for (Map.Entry<UUID, User> entry : response.users().entrySet()) {
        System.out.println("\t" + entry.getValue().getUsername()
            + ", currentLobbyId: "
            + entry.getValue().getCurrentLobbyId());
      }

      try {
        out.writeObject(response);
      } catch (IOException e) {
        throw new RuntimeException(e);
      }

      try {
        Thread.sleep(1000);
      } catch (InterruptedException e) {
        throw new RuntimeException(e);
      }
    }
  }

  public void updateMaps() {
    this.users = new ConcurrentHashMap<>(server.getUsers());
  }
}

Client.java:

import java.io.IOException;
import java.io.ObjectInputStream;
import java.net.Socket;
import java.util.Map;
import java.util.UUID;

public class Client {
  public static void main(String[] args) throws IOException, ClassNotFoundException {
    Socket socket = new Socket("localhost", 5050);

    ObjectInputStream in = new ObjectInputStream(socket.getInputStream());

    while (true) {
      Response response = (Response) in.readObject();

      System.out.println("Received user list:");
      for (Map.Entry<UUID, User> entry : response.users().entrySet()) {
        System.out.println("\t" + entry.getValue().getUsername()
            + ", currentLobbyId: "
            + entry.getValue().getCurrentLobbyId());
      }
    }
  }
}

User.java:

import java.io.Serializable;
import java.util.UUID;

public class User implements Serializable {
  private final UUID id;
  private final String username;
  private int currentLobbyId;

  public User(String username) {
    this.id = UUID.randomUUID();
    this.username = username;
  }

  public UUID getId() {
    return id;
  }

  public String getUsername() {
    return username;
  }

  public int getCurrentLobbyId() {
    return currentLobbyId;
  }

  public void setCurrentLobbyId(int newLobbyId) {
    currentLobbyId = newLobbyId;
  }
}

Response.java util class (I use this in my project as well, along with some added features that I have omitted here):

import java.io.Serializable;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;

public record Response(
    ConcurrentHashMap<UUID, User> users)
    implements Serializable {
}

To see the issue, run Server.java in one window and then Client.java in another, and compare their outputs. After 5 seconds a simulated lobby change happens. The sender prints the updated list correctly, but the receiver does not. Sorry if this MRE is a bit long, I wasn't too sure how far I could cut without removing important context.

  • 1
    "*I originally thought this could be some issue with passing by reference vs by value*" WDYM? Java is exclusively pass-by-value. – Michael Apr 19 '23 at 14:35
  • 3
    [mcve] please .. – kleopatra Apr 19 '23 at 14:37
  • @Michael I may be wrong but I understood that when working with objects and assigning one object to another (Object b = a, where a is an Object that already exists) then that assignment is done by reference instead of by value, as a is not copied and a new object created. That is what I understand at least. Am I incorrect? – Christo Swanepoel Apr 19 '23 at 14:56
  • @kleopatra Okay! It will take some time but I will add it as soon as I can. Thank you for your comment! – Christo Swanepoel Apr 19 '23 at 14:57
  • @ChristoSwanepoel pass-by-reference and pass-by-value are terms for function invocations, and Java's invocations are always pass-by-value. Your example is not a function invocation, it's assignment. You're right that there is no copy of an object there, but the value of the reference is copied. e.g. if `a` is a reference like `#123 -> SomeFoo` then the value `#123` is copied into `b`, which now points to the same instance. – Michael Apr 19 '23 at 15:05
  • @kleopatra I have done my best to add an MRE, I hope it suffices and is not too confusing – Christo Swanepoel Apr 20 '23 at 14:58
  • I suggest running wireshark and see if what is expected to be on the wire is on the wire. – Arfur Narf Apr 21 '23 at 00:26
  • _I have done my best to add an MRE_ thanks - actually it was good enough (after adding the missing User :) to reproduce your problem. Didn't come to analyse it yesterday and now you have found the solution yourself. Interesting .. – kleopatra Apr 21 '23 at 10:31
  • @kleopatra Oh my word! I can't believe I forgot to add the `User.java` class... I'm so sorry! I'll add it now, even though it isn't necessary, just so that I can be at ease. Also, thank you so much for all of your effort! I really appreciate it! :D – Christo Swanepoel Apr 21 '23 at 11:01

2 Answers2

2

I have found a solution to my problem! The problem occurred even with primitives, not just UUIDs. I came upon the idea to call out.reset() after out.writeObject() on the server side. This fixed my issue.

It seems that ObjectOutputStreams (or perhaps just streams in particular, unsure) are funky when it comes to writing the same object type over and over again, with minimally varying data. Whenever a new client connected, their first update was fine and was received correctly, hinting that something funky was happening only on occasion. Reading reset()'s documentation said it reset the stream as though it was a new ObjectOuputStream.

So, the main takeaway seems to be that, in the case of repeatedly writing the same object to an ObjectOutputStream, with little to no variation on the previously written object, it could cause issues. This is probably due to a quirk of, or perhaps a misunderstanding and/or ignorance regarding, the inner workings of an ObjectOuputStream.

What an interesting problem! Time to go write some Rust xD

  • `ObjectOutputStream` will not write the same object more than once, but just a reference to the original one. That's a specific feature of Java serialization (i.e. `ObjectOutputStream` specifically) and nothing general related to streams. – Joachim Sauer Apr 20 '23 at 18:12
  • @JoachimSauer Ahh that's super interesting! I wonder if there would be a better way to avoid this problem then instead of calling reset every time. Would a response counter or sequence number perhaps be enough to make Java realise that it isn't the same object as last time? – Christo Swanepoel Apr 21 '23 at 10:14
  • I'm not aware of any such solution outside of calling `reset()`. I think the `ObjectOutputStream` was primarily built with the idea of storing some objects to disk (i.e. all serialization happens at once) and not really optimized for the "continous stream of data" use case. – Joachim Sauer Apr 21 '23 at 10:27
-2

Re, "pass-by-reference," "pass-by-value". That's a question that has been absolutely beaten to death here. (See Is Java "pass-by-reference" or "pass-by-value"?)

Here's the heart of it:

void PBR_test() {
    String a = "Yellow";
    String b = "Black";

    foo(a,b);
    if (a == b) {
        System.out.println("same color");
    }
}

Is there any definition of foo(a,b) that can make PBR_test() print, "same color?"

If it's possible, then the args to foo are passed by reference.* If it's not possible, then the args to foo are passed by value. In Java, it's not possible. Java function arguments always are passed by value.


* Actually, there are some other esoteric pass-by-xxxxx that have been invented over the years, (Google for "Jensen's Device") but none of them has achieved anywhere near the level of popularity that pass-by-reference and pass-by-value have achieved.

Solomon Slow
  • 25,130
  • 5
  • 37
  • 57