3
\$\begingroup\$

I'm trying to write a very simple IMAP email client in Java.

There is one table per account with multiple multiline rows.

The last selected row of one of the tables should be displayed in an extra area when clicked.

I think I have a misunderstanding of how to properly model/design here, especially in the ActionListener of the displayButton. The code (for loop, etc.) to determine which row was selected last seems too complicated to me. Can you take a look at it?

The complete code is also available here.

import com.typesafe.config.Config;
import com.typesafe.config.ConfigFactory;
import jakarta.mail.*;
import jakarta.mail.internet.MimeMultipart;
import java.awt.*;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.table.AbstractTableModel;
import javax.swing.table.TableCellRenderer;
import org.jsoup.Jsoup;
import org.jsoup.nodes.Document;
import org.jsoup.safety.Safelist;
public class Main {
 record Account(
 int index, String host, int port, String user, String password, String inboxName) {}
 static class AccountConnection {
 private final Account account;
 private final Properties properties = new Properties();
 private Store emailStore;
 private Folder emailFolder;
 public AccountConnection(Account account) {
 this.account = account;
 properties.put("mail.store.protocol", "imap");
 properties.put("mail.imap.ssl.enable", true);
 properties.put("mail.imap.host", account.host());
 properties.put("mail.imap.port", account.port());
 }
 public List<Message> getMessages() throws Exception {
 if (emailStore == null || emailFolder == null) {
 emailStore = Session.getInstance(properties).getStore();
 emailStore.connect(account.user(), account.password());
 emailFolder = emailStore.getFolder(account.inboxName());
 emailFolder.open(Folder.READ_ONLY);
 }
 List<Message> l = new ArrayList<>();
 int c1 = emailFolder.getMessageCount();
 int c2 = 0;
 for (int i = c1; i > 0 && c2 < max_mails; i--, c2++) {
 l.add(emailFolder.getMessage(i));
 }
 return l;
 }
 public void close() throws Exception {
 System.out.println("Closing connection to: " + account.host());
 if (emailFolder != null) {
 emailFolder.close();
 }
 if (emailStore != null) {
 emailStore.close();
 }
 }
 }
 static class AccountManager {
 private static final List<Account> accounts = getAccounts();
 private final List<AccountConnection> connections = new ArrayList<>();
 public AccountManager() {
 for (Account account : accounts) {
 connections.add(new AccountConnection(account));
 }
 }
 public int getAccountCount() {
 return accounts.size();
 }
 public Map<Account, List<Message>> getMessages() throws Exception {
 Map<Account, List<Message>> messages = new LinkedHashMap<>();
 for (AccountConnection ac : connections) {
 messages.put(ac.account, ac.getMessages());
 }
 return messages;
 }
 public void close() throws Exception {
 for (AccountConnection ac : connections) {
 ac.close();
 }
 }
 private static List<Account> getAccounts() {
 String path = "accounts.conf";
 File file = new File(path);
 if (!file.exists()) {
 try (PrintWriter pw = new PrintWriter(file, StandardCharsets.UTF_8)) {
 // Adjust your accounts configuration here or in the accounts.conf file
 pw.println(
 """
 {
 "accounts":[
 {
 "index":1,
 "host":"foo",
 "port":993,
 "user":"foo",
 "password":"foo",
 "inboxName":"INBOX"
 },
 {
 "index":2,
 "host":"foo",
 "port":993,
 "user":"foo",
 "password":"foo",
 "inboxName":"INBOX"
 }
 ]
 }
 """);
 } catch (IOException e) {
 throw new RuntimeException(e);
 }
 }
 Config config = ConfigFactory.parseFile(file);
 List<Account> l = new ArrayList<>();
 for (Config c : config.getConfigList("accounts")) {
 l.add(
 new Account(
 c.getInt("index"),
 c.getString("host"),
 c.getInt("port"),
 c.getString("user"),
 c.getString("password"),
 c.getString("inboxName")));
 }
 return l;
 }
 }
 static class MultiLineTableModel extends AbstractTableModel {
 private final List<String[]> data = new ArrayList<>();
 public void addRow(Message message) {
 try {
 data.add(new String[] {getDate(message), getFrom(message), getSubject(message)});
 } catch (Exception e) {
 throw new RuntimeException(e);
 }
 fireTableDataChanged();
 }
 public void clearRows() {
 data.clear();
 fireTableDataChanged();
 }
 @Override
 public int getRowCount() {
 return data.size();
 }
 @Override
 public int getColumnCount() {
 return 1;
 }
 @Override
 public Object getValueAt(int rowIndex, int columnIndex) {
 return data.get(rowIndex);
 }
 @Override
 public String getColumnName(int column) {
 return "Messages";
 }
 @Override
 public Class<?> getColumnClass(int columnIndex) {
 return String[].class;
 }
 }
 static class MultiLineTableCellRenderer extends JList<String> implements TableCellRenderer {
 @Override
 public Component getTableCellRendererComponent(
 JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
 if (value instanceof String[]) {
 setListData((String[]) value);
 }
 if (isSelected) {
 setBackground(UIManager.getColor("Table.selectionBackground"));
 } else {
 setBackground(UIManager.getColor("Table.background"));
 }
 return this;
 }
 }
 static class MultilineTable {
 private final MultiLineTableModel model = new MultiLineTableModel();
 private final JTable table = new JTable(model);
 private int lastIndex = -1;
 private long lastIndexTime = -1;
 public MultilineTable() {
 table.setDefaultRenderer(String[].class, new MultiLineTableCellRenderer());
 table.setRowHeight((table.getRowHeight() + 3) * 3);
 table
 .getSelectionModel()
 .addListSelectionListener(
 e -> {
 if (!e.getValueIsAdjusting()) {
 updateLastIndex();
 }
 });
 }
 private void updateLastIndex() {
 lastIndex = table.getSelectedRow();
 lastIndexTime = System.currentTimeMillis();
 }
 }
 private static Map<Account, List<Message>> messages = new LinkedHashMap<>();
 private static int max_mails = 6;
 private static String getDate(Message message) throws Exception {
 return message.getSentDate().toString();
 }
 private static String getFrom(Message message) throws Exception {
 StringBuilder sb = new StringBuilder();
 for (Address a : message.getFrom()) {
 sb.append(a.toString());
 }
 return sb.toString();
 }
 private static String getSubject(Message message) throws Exception {
 return message.getSubject();
 }
 private static String getContent(Message message) throws Exception {
 StringBuilder sb = new StringBuilder();
 if (message.isMimeType("text/plain")) {
 sb.append(message.getContent().toString());
 }
 if (message.isMimeType("multipart/*")) {
 MimeMultipart mimeMultipart = (MimeMultipart) message.getContent();
 for (int i = 0; i < mimeMultipart.getCount(); i++) {
 Document document = Jsoup.parse(mimeMultipart.getBodyPart(i).getContent().toString());
 document.outputSettings(new Document.OutputSettings().prettyPrint(false));
 document.select("br").append("\\n");
 document.select("p").prepend("\\n\\n");
 String s = document.html().replaceAll("\\\\n", "\n");
 sb.append(
 Jsoup.clean(s, "", Safelist.none(), new Document.OutputSettings().prettyPrint(false)));
 }
 }
 return sb.toString();
 }
 private static void createGUI() {
 AccountManager am = new AccountManager();
 JButton reloadButton = new JButton("Reload");
 JTextField maxField = new JTextField(Integer.toString(max_mails));
 maxField.setPreferredSize(new Dimension(30, 22));
 JButton displayButton = new JButton("Display");
 MultilineTable[] multilineTables = new MultilineTable[am.getAccountCount()];
 for (int i = 0; i < multilineTables.length; i++) {
 multilineTables[i] = new MultilineTable();
 }
 JTextArea area = new JTextArea();
 area.setLineWrap(true);
 area.setWrapStyleWord(true);
 area.setEditable(false);
 area.setFont(new Font("Monospaced", Font.PLAIN, 12));
 JPanel bp1 = new JPanel(new FlowLayout(FlowLayout.LEFT));
 bp1.add(reloadButton);
 bp1.add(maxField);
 JPanel bp2 = new JPanel(new FlowLayout(FlowLayout.LEFT));
 bp2.add(displayButton);
 JPanel p1 = new JPanel(new GridLayout(1, 2));
 p1.add(bp1);
 p1.add(bp2);
 JPanel tablesPanel = new JPanel(new GridLayout(multilineTables.length, 1));
 for (MultilineTable t : multilineTables) {
 tablesPanel.add(new JScrollPane(t.table));
 }
 JPanel p2 = new JPanel(new GridLayout(1, 2));
 p2.add(tablesPanel);
 p2.add(new JScrollPane(area));
 JFrame f = new JFrame("MMClient");
 f.setLayout(new BorderLayout());
 f.add(p1, BorderLayout.NORTH);
 f.add(p2, BorderLayout.CENTER);
 f.setSize(1200, 850);
 f.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
 f.setVisible(true);
 maxField
 .getDocument()
 .addDocumentListener(
 new DocumentListener() {
 public void changedUpdate(DocumentEvent e) {
 upd();
 }
 public void removeUpdate(DocumentEvent e) {
 upd();
 }
 public void insertUpdate(DocumentEvent e) {
 upd();
 }
 public void upd() {
 String s = maxField.getText();
 if (!s.isBlank()) {
 try {
 max_mails = Integer.parseInt(s);
 } catch (NumberFormatException ignore) {
 }
 }
 }
 });
 reloadButton.addActionListener(
 e -> {
 try {
 messages = am.getMessages();
 int i = 0;
 for (Map.Entry<Account, List<Message>> entry : messages.entrySet()) {
 multilineTables[i].model.clearRows();
 for (Message m : entry.getValue()) {
 multilineTables[i].model.addRow(m);
 }
 i++;
 }
 } catch (Exception ex) {
 throw new RuntimeException(ex);
 }
 });
 displayButton.addActionListener(
 e -> {
 try {
 int i = -1;
 int j = -1;
 long t = -1;
 for (int k = 0; k < multilineTables.length; k++) {
 MultilineTable mul = multilineTables[k];
 if (mul.lastIndex >= 0 && mul.lastIndexTime > t) {
 i = k;
 j = mul.lastIndex;
 t = mul.lastIndexTime;
 }
 }
 if (i >= 0) {
 Message m = messages.get(am.connections.get(i).account).get(j);
 area.setText(
 getDate(m)
 + "\n\n"
 + getFrom(m)
 + "\n\n"
 + getSubject(m)
 + "\n\n"
 + getContent(m));
 area.setCaretPosition(0);
 }
 } catch (Exception ex) {
 throw new RuntimeException(ex);
 }
 });
 f.addWindowListener(
 new WindowAdapter() {
 @Override
 public void windowClosing(WindowEvent e) {
 try {
 am.close();
 } catch (Exception ex) {
 throw new RuntimeException(ex);
 }
 }
 });
 }
 public static void main(String[] args) {
 try {
 createGUI();
 } catch (Exception e) {
 JOptionPane.showMessageDialog(
 null,
 e.getMessage() + "\n\n" + Arrays.toString(e.getStackTrace()),
 "Error",
 JOptionPane.ERROR_MESSAGE);
 }
 }
}

Dependencies used:

dependencies {
 // https://mvnrepository.com/artifact/com.sun.mail/jakarta.mail
 implementation 'com.sun.mail:jakarta.mail:2.0.1'
 // https://mvnrepository.com/artifact/org.jsoup/jsoup
 implementation 'org.jsoup:jsoup:1.18.1'
 // https://mvnrepository.com/artifact/com.typesafe/config
 implementation 'com.typesafe:config:1.4.3'
}
asked Aug 4, 2024 at 4:40
\$\endgroup\$
6
  • 1
    \$\begingroup\$ I think you could benefit from using the information coming inside the event e. It usually brings the info of the object in which the event (usually a click) was generated, and it should contain which row from which table was targeted \$\endgroup\$ Commented Aug 12, 2024 at 14:28
  • 1
    \$\begingroup\$ In this way you would not write a for loop to find the desired object. Instead you would just write how to get the instance from the event in Swing, maybe cast that into a MultilineTable and then set the attributes on that instance. \$\endgroup\$ Commented Aug 12, 2024 at 14:32
  • \$\begingroup\$ @madtyn And how do you then calculate click position -> table and table row? \$\endgroup\$ Commented Aug 12, 2024 at 15:19
  • \$\begingroup\$ Look while using the debugger what info does the 'e' variable contain. If it comes with an object, if the object is of type Table, type Row or what. It should be quite natural. You could also check the swing documentation to see what's that info like. \$\endgroup\$ Commented Aug 12, 2024 at 17:36
  • \$\begingroup\$ Looks like e.getSource() should return the object where the event happened. If the row is contained in that object. getting to the row should be easy. \$\endgroup\$ Commented Aug 12, 2024 at 17:42

1 Answer 1

3
+50
\$\begingroup\$

The major issue is that there's no proper Separation of concerns.

All application components are stuffed into the Main class, with no concerted effort to delineate clearly their areas of concerns. You were rather focusing on getting things working.

The consequences are:

  • There's no encapsulation, since nested classes are transparent to each other. And there are multiple places in the code where you're cheated by leveraging broken encapsulation to access private elements.
  • In such settings it's not immediately obvious if there are missing abstractions (and in fact there are), or whether the existing abstractions serve you well (are useful and have clear APIs).
  • It's difficult to make changes in such code when various pieces of functionality are intertwined together. So, readability and maintainability is suffering.

Additionally, there are some issues with exception handling and code elements with high cognitive load such as long hefty methods, multiline lambda expressions and unclear names.

Separation of Concerns - General approach

To structure the code properly, things are closely related should be placed together, they will change and evolve together. Things that differ should reside apart.

These are the main areas of responsibilities in your application:

  • Connecting to an email-provider and retrieving email messages. Related components: AccountManager, AccountConnection.

  • GUI. Related components: MultiLineTableModel, MultiLineTableCellRenderer, MultilineTable.

  • Loading and storing Configurations: account-related setting (host, port of email-provider, username, password, etc.) and GUI-related setting (such as font size, preferred number of messages to display, etc.). There are no components assigned with these responsibilities in the code, but there's a humongous method getAccounts() that reads and writes hard-coded account-data which should not belong to production code. Instead, there should be a separate self-contained component for handling this task, decoupled from AccountManager.

  • A component responsible for dialing with multipart message data. You haven't introduced it explicitly.

  • Mail client serves as glue putting together account management, configurations, multipart-parser and GUI-related classes. This component is not explicitly present in the code. It should contain as its instance fields a Map of messages, an AccountManager, an instance of each component describing user's configurations and a number of messages to display. And it should be responsible for starting the GUI.

Start with reorganizing your code. Each group of components deserves its own package and every component its own file.

You'll immediately see that you've created accidental coupling through the means of broken encapsulation. For instance:

  • AccountConnection.getMessages() uses internally max_mails variable (by the way, this name is not aligned with language naming convention) which should reside in the mail client, and hence should be passed to the getMessages() method as a parameter.

  • multilineTables[i].model.addRow(m) - obviously MultiLineTableModel should feature its own method addRow(). Object-orientation is about exposing useful domain-specific behavior, not exposing internal data.

  • multilineTables[i].model.clearRows() - again, MultiLineTableModel is lacking a method.

  • messages.get(am.connections.get(i).account).get(j) - when you operate with your domain objects like this, that's a clear indicator of a design flaw. In this case, I suppose, the fix requires revising the overall design approach of dialing with messages.

NOTE: we're not talking about merely putting classes into separate files. Although, it will already expose issues with encapsulation. The ultimate goal - a system comprised of loosely coupled, cohesive component. I.e. each component has its clear purpose reflected in its API and none of the components encroaches on the areas of responsibilities it shouldn't be concerned with.

The next step is to eradicate accidental coupling. Classes from the email domain should not be aware of any demands dictated by GUI. In turn, GUI elements should not know anything about fetching data from email providers, or how to parse it, or load configs.

In the code you shared MultiLineTableModel actually fetches email data via Message.getSentDate(), Message.getSubject(), etc. since these methods trigger IO operation when on their first invocation. And even if the data has been already fetched, why should a GUI-class be familiar with jakarta.mail.Message?

Instead, we can build MultiLineTableModel around a data type specifically tailored to the current needs of GUI. The MailClient class, which is meant to bring all other components together, will be responsible for converting jakarta.mail.Message to the email representation intended for GUI. Fetching of email-content can be implemented lazily, but for simplicity I'll do it eagerly (the reader can change the implementation as an exercise).

And as I said, you'll also benefit from decoupling the functionality for parsing multipart message and loading / saving configurations because such elements can be easily unit-tested.

Practical example - Displaying Mail Content

The original way of approaching this task by juggling with indices is very convoluted and error-prone, let alone the hacky message look up (see the code inside displayButton.addActionListener(e -> {...}) which operates by mutating the indices maintained by MultilineTable).

Furthermore, examining the GUI from the user's perspective, I found Display-button to be rather a redundant nuisance, than a feature facilitating better user experience.

It'll be more convenient and intuitive to simply select an email on the left and see it's content displayed on the right without any additional actions, like clicking on the display button.

The implementation shown below is based on JTable.getSelectedRow() and there's no diplay button.

Snippet from MailClients code that kicks off the GUI:

MultilineTable[] multilineTables = new MultilineTable[accountManager.getAccountCount()];
JTextArea area = new JTextArea();
...
for (var table : multilineTables) {
 table.addSelectionListener(event -> showSelectedMessage(table, event, area));
}
private static void showSelectedMessage(MultilineTable table, ListSelectionEvent event, JTextArea area) {
 if (!event.getValueIsAdjusting()) {
 area.setText(
 table.getSelectedValue().getFullMessage()
 );
 area.setCaretPosition(0);
 }
}

Conversion to MessageModel:

public static MessageModel toMessageModel(Message message)
 throws MessagingException, IOException {
 
 return new MessageModel(
 toUsersLocalDateTime(message.getSentDate()),
 addressAsString(message),
 message.getSubject(),
 getContent(message)
 );
}
private static String addressAsString(Message message) throws MessagingException {
 return Arrays
 .stream(message.getFrom())
 .map(String::valueOf)
 .collect(joining(" "));
}
private static LocalDateTime toUsersLocalDateTime(Date sentDate) {
 return LocalDateTime.ofInstant(
 sentDate.toInstant(),
 ZoneId.systemDefault()
 );
}

It's always a good idea to represent date and time using proper types not as strings, jakarta.mail gives as legacy java.util.Date, hence conversion to LocalDateTime

MessageModel itself:

public record MessageModel(LocalDateTime dateTime,
 String from,
 String subject,
 String content) {
 
 public Object[] getPreview() {
 return new Object[] {dateTime, from, subject};
 }
 
 public String getFullMessage() {
 return String.join(
 System.lineSeparator().repeat(2),
 dateTime.toString(),
 from,
 subject,
 content
 );
 }
}

GUI components

Here List<MessageModel> is in the core of the table model, not a list of string arrays.

public class MultiLineTableModel extends AbstractTableModel {
 private final List<MessageModel> messages = new ArrayList<>();
 
 public void addRow(MessageModel message) {
 messages.add(message);
 fireTableDataChanged();
 }
 
 public void clearRows() {
 messages.clear();
 fireTableDataChanged();
 }
 
 @Override
 public int getRowCount() {
 return messages.size();
 }
 
 @Override
 public int getColumnCount() {
 return 1;
 }
 
 public MessageModel getValue(int rowIndex) {
 return messages.get(rowIndex);
 }
 
 @Override
 public Object getValueAt(int rowIndex, int columnIndex) {
 if (columnIndex != 0) {
 throw new IllegalArgumentException(
 "column index %s is out of bounds for one-column table".formatted(columnIndex)
 );
 }
 return getValue(rowIndex);
 }
 
 @Override
 public String getColumnName(int column) {
 return "Messages";
 }
 
 @Override
 public Class<?> getColumnClass(int columnIndex) {
 return MessageModel.class;
 }
}
public class MultiLineTableCellRenderer extends JList<Object> implements TableCellRenderer {
 @Override
 public Component getTableCellRendererComponent(
 JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) {
 if (value instanceof MessageModel mailModel) {
 setListData(mailModel.getPreview());
 }
 
 if (isSelected) {
 setBackground(UIManager.getColor("Table.selectionBackground"));
 } else {
 setBackground(UIManager.getColor("Table.background"));
 }
 
 return this;
 }
}
public class MultilineTable {
 private final MultiLineTableModel model = new MultiLineTableModel();
 private final JTable table = new JTable(model);
 
 public MultilineTable() {
 table.setDefaultRenderer(MessageModel.class, new MultiLineTableCellRenderer());
 table.setRowHeight((table.getRowHeight() + 3) * 3);
 }
 
 public void addSelectionListener(ListSelectionListener listener) {
 table
 .getSelectionModel()
 .addListSelectionListener(listener);
 }
 
 public MessageModel getSelectedValue() {
 return model.getValue(table.getSelectedRow());
 }
}

And here's the result with dummy data:

screen of the modified GUI

I would advice adding a label next to the field for setting the maximum number of mail per account to display.

Also, when tables hosting emails from diffent accounts are stacked upon one another, it doesn't look very nicely, since selections in each table are working independantly you might observe multiple selected emails on the left (one per account), which is confusing. Consider a posibility of using a dropdown menu to swich between the user accounts instead.

Exception Handling / Displaying meaningful Error-messages

Key points:

  • Avoid catching supertypes Exception, Throwable, such catch block might swallow a runtime exception caused by a bug. Target specific exceptions (such as MessagingException).
  • Avoid declaring having methods declaring to throw Exception type. If a method in the parent class declares throwing Exception in it's throws clause you're to override in such a way that it either: throws the same type of exception, throws a subtype, throws a runtime exception is entirely safe, the only prohibited thing will be to declare throwing Throwable (that's basically the Liskov substitution principle in action, the child adheres to the contract declared by parent, so it can always be used instead of its parent and behavior remains predictable). Prefer eliminating checked exceptions early when possible, instead spreading them throughout the code (e.g.: AccountConnection.getMessages()).
  • Communicate the issues to the user, if their input is invalid let them know.
  • Event dispatch thread will be restarted if it goes down due to an uncaught exception, so that GUI remains responsive (i.e. the application will not terminate). Re-throwing caught exceptions without logging them in the absence of a global exception handler is pointless.
  • An empty catch block is always fishy. Take a closer look at it, and ask yourself why would you want this exception to be swallowed silently.

The bottom line: a reasonable thing to do in case of an exception in the EDT is to log the exception and, possibly, display a message to the user if we can tell them something meaningful (don't show them stack trace). For instance, when from we have a network-related exception, we can prompt the user to retry. Re-throwing a runtime exception wrapping a checked exception in the EDT make only we have established a global exception handling mechanism and the appropriate action will be taken downstream (otherwise the EDT goes down and gets launched again, and the exception passes unnoticed).

In this application, examples of where we can potentially benefit from wrapping and re-throwing + using a global exception handler are methods concerned with fetching mail data, opening and closing a connection. All they, according to jakarta.mail documentation, might throw either MessagingException, or IOException (or both). And I suggest catching only these exceptions, not supertype Exception which encompasses runtime exceptions as well, thus might accidentally hide bugs.

The simplest to create a global exception handler is to make use of the method Thread.setDefaultUncaughtExceptionHandler(). Important caveat: to handle exception in the Event dispatch thread, we need to attach the handler while running in the EDT, not the Main thread. Here's an illustration, on how it might be done

SwingUtilities.invokeAndWait(() ->
 Thread
 .currentThread()
 .setUncaughtExceptionHandler(MailClient::handleUncaught)
);
private static void handleUncaught(Thread thread, Throwable throwable) {
 logger.error("Uncaught Throwable in the EDT", throwable);
 
 if (throwable instanceof AccountConnectionException e) {
 logger.error(
 "Uncaught Exception in the EDT while communicating with the Email provider",
 e.getCause() // in this case we are interested in the cause, not in the wrapper
 ); 
 // show error message using JOptionPane
 }
}

Exception that be only instantiated by being wrapped around either MessagingException or IOException:

public class AccountConnectionException extends RuntimeException {
 private AccountConnectionException(Exception cause) {
 super(cause);
 }
 
 public static AccountConnectionException wrap(MessagingException cause) {
 return new AccountConnectionException(cause);
 }
 
 public static AccountConnectionException wrap(IOException cause) {
 return new AccountConnectionException(cause);
 }
}

Another, a more involved option to dial with uncaught exceptions in the EDT is to extend EventQueue overriding its method dispatchEvent() by placing exception handling logic into it (see example ).

Avoid multiline Lambdas

Lambda expressions should be glue code. Two lines may be too many.

Venkat Subramaniam, Java Champion and author of several books on JVM Languages

Multiline lambda expressions bare a lot of cognitive load and hard to follow. Always consider introducing a well-named method containing all the heavy logic which you can either invoke from a lambda, or use in a method reference (see ListSelectionListener implemented as a lambda in the beginning of the answer).

Follow the Java Language naming convention

Variable names should be short yet meaningful. The choice of a variable name should be mnemonic- that is, designed to indicate to the casual observer the intent of its use. One-character variable names should be avoided except for temporary "throwaway" variables.

The name of a code element should communicate its intent, be helpful in giving you insight into what the code does, and not be a riddle that requires additional brainpower to crack.

Name am isn't a nice one, it's not a throwaway variable, and it'll be more helpfull to see it written as accountManager, especially when it appeas in the midst of a convoluted code.

answered Aug 14, 2024 at 4:35
\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.