Java Developer Roadmap

Last updated 2 months ago

This is a step by step guide to go from the basic to more advanced java topics.

Prerequisites

This guide assumes that you:

  • Have installed JDK 8

  • Have a text edtior or IDE to write Java code

  • Have basic knowledge of java syntax

Who should read this guide?

This guide is for beginners who want to make a little more advanced java apps.

What will we build?

For this first guide we will build a phonebook app where you can search for contacts.

  • In the first chapter we will build a simple console application.

  • In the second chapter we will build a GUI on top of it

  • In the third chapter we will store our contacts in a SQL Database

  • In the fourth chapter we will transform our app into a Spring Webapp.

Part One - Building the phonebook console app

Defining our Model

In our app we want to handle (add / delete / search) contacts, but what exactly is a contact? First we need to define our Model of a Contact, for now we create a class called Contact.

Contact.java
package com.juniordev.phonebook.Model;
public class Contact {
private String name;
private String number;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getNumber() {
return number;
}
public void setNumber(String number) {
this.number = number;
}
public Contact(String name, String number){
this.name = name;
this.number = number;
}
@Override
public String toString() {
return this.name+";"+this.number+";";
}
}

So whats going on in this class? We define to member variables for the Name and the Number of a Contact. To encapsulate our data we make the member variables private and expose public getter and setter methods. Also we override the toString() method to display the name and number of a contact.

Adding Behavior

Now we have a model of our contact, but we have no methods that can operate with it. Lets create a class called PhonebookService.java - that will handle all our operations on a contact.

PhonebookService.java
package com.juniordev.phonebook;
import com.juniordev.phonebook.Model.Contact;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Scanner;
import java.util.stream.Collectors;
public class PhonebookService {
private static final int NAME = 0;
private static final int NUMBER = 1;
List<Contact> contacts = new ArrayList<Contact>();
public List<Contact> getContacts() {
return contacts;
}
public void setContacts(List<Contact> contacts) {
this.contacts = contacts;
}
public void addContact (Contact contactToAdd){
this.getContacts().add(contactToAdd);
}
public void deleteContact (Contact contactToDelete){
this.getContacts().remove(contactToDelete);
}
public List<Contact> findContactsByName(String name){
return this.getContacts().stream().filter((Contact c )-> c.getName().indexOf(name) > -1).collect(Collectors.toList());
}
public List<Contact> findContactsByNumber(String number) {
return this.getContacts().stream().filter(c -> c.getNumber().indexOf(number) > -1).collect(Collectors.toList());
}
public PhonebookService (String fileName){
loadFromFile(fileName);
}
public PhonebookService (List<Contact> contactList){
if (contactList != null){
this.contacts = contactList;
}
}
public List<Contact> loadFromFile (String fileName){
List<Contact> contacts = new ArrayList<>();
try (Scanner scanner = new Scanner(new FileInputStream(fileName))){
while (scanner.hasNextLine()){
String[] contactNameAndNumber = scanner.nextLine().split(";");
Contact newContact = new Contact(contactNameAndNumber[NAME],contactNameAndNumber[NUMBER]);
contacts.add(newContact);
}
} catch (FileNotFoundException e) {
System.out.println("File could not be found.");
}
return contacts;
}
public void saveToFile(String fileName){
StringBuilder sb = new StringBuilder();
try (BufferedWriter bw = new BufferedWriter(new FileWriter(fileName))){
for (Contact c : this.getContacts()){
sb.append(c.toString());
sb.append(System.lineSeparator());
}
bw.write(sb.toString());
} catch (IOException e) {
System.out.println("Error saving file.");
}
}
}

Lets look at each method: First we have simple setter and getter methods to work with a list of contacts.

public List<Contact> getContacts() {
return contacts;
}
public void setContacts(List<Contact> contacts) {
this.contacts = contacts;
}

We also have the option to add and delete a single contact.

public void addContact (Contact contactToAdd){
this.getContacts().add(contactToAdd);
}
public void deleteContact (Contact contactToDelete){
this.getContacts().remove(contactToDelete);
}

Now things become interesting, we use the new Java 8 Stream API and lambda expressions to search for contacts

public List<Contact> findContactsByName(String name){
return this.getContacts().stream().filter((Contact c )-> c.getName().indexOf(name) > -1).collect(Collectors.toList());
}
public List<Contact> findContactsByNumber(String number) {
return this.getContacts().stream().filter(c -> c.getNumber().indexOf(number) > -1).collect(Collectors.toList());
}

We use the .stream() method to get access to the .filter() method which we can use to filter results based on a given lambda expression. You might need to get used to the lambda syntax: on the left hand side of the arrow ( -> ) operator you specify your object and on the right hand side you declare the boolean expression which every contact in the list will get checked against.

To read more about lambda expressions go to http://junior-dev.com/ and search for lambda tutorials.

Challenge

We already implemented a findContactsByName and a findContactsByNumber method, try to build a method findContacts(String searchTerm) which will check if the searchTerm is contained in the name or number variable.

We also implement methods for saving and loading our contacts to a simple CSV File:

public void saveToFile(String fileName){
StringBuilder sb = new StringBuilder();
try (BufferedWriter bw = new BufferedWriter(new FileWriter(fileName))){
for (Contact c : this.getContacts()){
sb.append(c.toString());
sb.append(System.lineSeparator());
}
bw.write(sb.toString());
} catch (IOException e) {
System.out.println("Error saving file.");
}
}

We use a StringBuilder to convert our list of contacts to a single string where each name and number is seperated with a delimitor ( ; ). Note that we have overriden the toString() method of our Contact class to match the csv format.

We also implement a loadFromFile method to make used of our saved csv file.

public List<Contact> loadFromFile (String fileName){
List<Contact> contacts = new ArrayList<>();
try (Scanner scanner = new Scanner(new FileInputStream(fileName))){
while (scanner.hasNextLine()){
String[] contactNameAndNumber = scanner.nextLine().split(";");
Contact newContact = new Contact(contactNameAndNumber[NAME],contactNameAndNumber[NUMBER]);
contacts.add(newContact);
}
} catch (FileNotFoundException e) {
System.out.println("File could not be found.");
}
return contacts;
}

We used the try with resources statement which handles the closing of our resources for us. If you are not familiar with it, read it up on http://junior-dev.com

Finally we implement two constructors: One takes a filename and uses our loadFromFile method. The other one accepts a list of contacts.

public PhonebookService (String fileName){
loadFromFile(fileName);
}
public PhonebookService (List<Contact> contactList){
if (contactList != null){
this.contacts = contactList;
}
}

Lets add some Tests

So we implemented a bunch of methods but we cant actually say for sure that all of them are working as intended. We use UnitTests to check if our methods behave as intended.

PhonebookServiceTest.java
package com.juniordev.phonebook;
import com.juniordev.phonebook.Model.Contact;
import org.junit.Assert;
import org.junit.Before;
import org.junit.Test;
import java.util.ArrayList;
import java.util.List;
public class PhonebookServiceTest {
PhonebookService phonebookService;
@Before
public void setUp(){
ArrayList<Contact> contacts = new ArrayList<>();
contacts.add(new Contact("Jeff","1234"));
contacts.add(new Contact("Joffrey","3456"));
this.phonebookService = new PhonebookService(contacts);
}
@Test
public void testFindByName(){
List<Contact> result = phonebookService.findContactsByName("Jeff");
Assert.assertEquals("Result size not equal" ,1 ,result.size());
Assert.assertTrue(result.get(0).getName().equals("Jeff"));
}
@Test
public void testFindByNumber(){
List<Contact> result = phonebookService.findContactsByNumber("3456");
Assert.assertEquals("Result size not equal" ,1 ,result.size());
Assert.assertTrue(result.get(0).getNumber().equals("3456"));
}
@Test
public void testFindMultipleByNumber(){
List<Contact> result = phonebookService.findContactsByNumber("34");
Assert.assertEquals("Result size not equal" ,2 ,result.size());
}
@Test
public void testFindMultipleByName(){
List<Contact> result = phonebookService.findContactsByName("ff");
Assert.assertEquals("Result size not equal" ,2 ,result.size());
}
@Test
public void testSaveAndLoadFromFile(){
this.phonebookService.saveToFile("data.csv");
List<Contact> result = this.phonebookService.loadFromFile("data.csv");
Assert.assertEquals("Loaded all contacts",2,result.size());
}
}

We use the @Before annotation and a setUp() method that runs before any tests are executed

@Before
public void setUp(){
ArrayList<Contact> contacts = new ArrayList<>();
contacts.add(new Contact("Jeff","1234"));
contacts.add(new Contact("Joffrey","3456"));
this.phonebookService = new PhonebookService(contacts);
}

Now we check if our findBy methods work as expected:

@Test
public void testFindByName(){
List<Contact> result = phonebookService.findContactsByName("Jeff");
Assert.assertEquals("Result size not equal" ,1 ,result.size());
Assert.assertTrue(result.get(0).getName().equals("Jeff"));
}
@Test
public void testFindByNumber(){
List<Contact> result = phonebookService.findContactsByNumber("3456");
Assert.assertEquals("Result size not equal" ,1 ,result.size());
Assert.assertTrue(result.get(0).getNumber().equals("3456"));
}

We have two contacts in our phonebookService one of them called "Jeff" so we expect to find one contact with the name "Jeff". We check that with the assertEquals and the assertTrue method.

Also note that we annotated all our test methods with @Test. This is necessary to run our test cases later on with junit.

Now we check if we also find multiple results:

@Test
public void testFindMultipleByNumber(){
List<Contact> result = phonebookService.findContactsByNumber("34");
Assert.assertEquals("Result size not equal" ,2 ,result.size());
}
@Test
public void testFindMultipleByName(){
List<Contact> result = phonebookService.findContactsByName("ff");
Assert.assertEquals("Result size not equal" ,2 ,result.size());
}

We should also test our save and load method, so lets do that:

@Test
public void testSaveAndLoadFromFile(){
this.phonebookService.saveToFile("data.csv");
List<Contact> result = this.phonebookService.loadFromFile("data.csv");
Assert.assertEquals("Loaded all contacts",2,result.size());
}

Challenge

Think of your own test cases and write test for that. E.g. what should happen if we search for a contact that we have not stored in our list. If you want to read more about unit tests go to http://junior-dev.com and search for JUnit

Final Challenge

All our methods are implemented, now we just need to implement a main method which accepts user input for adding, deleting and searching for contacts.

Try it on your own, if you get stuck look on junior-dev.com or google your problem. The full code will be available with the release of part two.

Whats next?

This concludes the first part of this guide. In the next part we wil build a GUI with Java Swing ontop of our current application. To get notified about the release of the next part please go to http://junior-dev.com and subscribe to the newsletter or check back regulary.

Part Two - Building a GUI for our phonebook app

Java Swing

For our GUI we will use the Java Swing Framework. On http://junior-dev.com you will find some good tutorials to learn the basics of java swing. In this guide you will get a high level overview and learn how to build a basic GUI with java.

Creating our GUI Class

Lets create a class PhonebookGUI which will extend the class JFrame. A JFrame is the basic container for our GUI window.

PhonebookGUI.java
package com.juniordev.phonebook.GUI;
import com.juniordev.phonebook.Model.Contact;
import com.juniordev.phonebook.Service.PhonebookService;
import javax.swing.*;
import javax.swing.table.DefaultTableModel;
import javax.swing.table.TableColumn;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
public class Phonebook extends JFrame {
private static final int PREF_WINDOW_WIDTH = 800;
private static final int PREF_WINDOW_HEIGHT = 600;
private static final String APP_NAME = "PhoneBook App";
private static final int PREF_WIDTH = 600;
private static final int PREF_HEIGHT = 20;
private static final String TABLE = "table";
private static PhonebookService phonebookService;
private static DefaultTableModel defaultTableModel;
private static final String SEARCH_FIELD = "searchField";
private static final String ADD_NAME = "addName";
private static final String ADD_NUMBER = "addNumber";
private static final HashMap<String,Component> componentMap = new HashMap<>();
public static void main(String[] args) {
Phonebook phonebook = new Phonebook();
List<Contact> contactList = new ArrayList<>();
contactList.add(new Contact("John","123"));
contactList.add(new Contact("Moe","234"));
phonebookService = new PhonebookService(contactList);
phonebook.setTitle(APP_NAME);
phonebook.getContentPane().setLayout(new BoxLayout(phonebook.getContentPane(),BoxLayout.PAGE_AXIS));
init(phonebook, phonebookService);
phonebook.setSize(PREF_WINDOW_WIDTH,PREF_WINDOW_HEIGHT);
phonebook.setVisible(true);
}
private static void init(Phonebook phonebook, PhonebookService phonebookService) {
JLabel jLabel = new JLabel(APP_NAME);
JPanel searchPanel = createSearchPanel();
JPanel addPanel = createAddPanel();
JScrollPane pane = createTablePane(phonebookService);
phonebook.add(jLabel);
phonebook.add(searchPanel);
phonebook.add(addPanel);
phonebook.add(pane);
for(Component c : searchPanel.getComponents()){
componentMap.put(c.getName(),c);
}
}
private static JScrollPane createTablePane(PhonebookService phonebookService) {
String[] columnNames = {"Name",
"Number","Delete"};
defaultTableModel = new DefaultTableModel(useContactsForTable(phonebookService.getContacts()),columnNames);
JTable jTable = new JTable(defaultTableModel);
componentMap.put(TABLE,jTable);
JScrollPane pane = new JScrollPane(jTable);
jTable.setAutoCreateRowSorter(true);
TableColumn delteCol = jTable.getColumn("Delete");
DeleteButton deleteButton = new DeleteButton("Delete");
deleteButton.addButtonListener(new DeleteButtonListener() {
@Override
public void buttonClicked(int row, int col) {
delete(row,col);
}
});
delteCol.setCellEditor(deleteButton);
delteCol.setCellRenderer(deleteButton);
jTable.setFillsViewportHeight(false);
return pane;
}
private static JPanel createAddPanel() {
JPanel addPanel = new JPanel();
JButton addButton = new JButton("Add");
addButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent actionEvent) {
addContact();
}
});
JTextField addName = new JTextField();
componentMap.put(ADD_NAME,addName);
addName.setMaximumSize(new Dimension(PREF_WIDTH,PREF_HEIGHT));
JLabel nameLabel = new JLabel("Name");
nameLabel.setLabelFor(addName);
JTextField addNumber = new JTextField();
componentMap.put(ADD_NUMBER,addNumber);
JLabel numberLabel = new JLabel("Number");
addNumber.setMaximumSize(new Dimension(PREF_WIDTH,PREF_HEIGHT));
addPanel.setLayout(new BoxLayout(addPanel,BoxLayout.LINE_AXIS));
addPanel.add(nameLabel);
addPanel.add(addName);
addPanel.add(numberLabel);
addPanel.add(addNumber);
addPanel.add(addButton);
return addPanel;
}
private static JPanel createSearchPanel() {
JPanel searchPanel = new JPanel();
searchPanel.setLayout(new BoxLayout(searchPanel,BoxLayout.LINE_AXIS));
searchPanel.setPreferredSize(new Dimension(PREF_WIDTH,PREF_HEIGHT));
JButton searchButton = new JButton("Search");
searchButton.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
search();
}
});
JLabel searchLabel = new JLabel("Search");
JTextField searchField = new JTextField();
componentMap.put(SEARCH_FIELD,searchField);
searchLabel.setLabelFor(searchField);
searchField.setMaximumSize(new Dimension(PREF_WIDTH,PREF_HEIGHT));
searchPanel.add(searchLabel);
searchPanel.add(searchField);
searchPanel.add(searchButton);
return searchPanel;
}
private static String[][] useContactsForTable(List<Contact> contacts) {
if (contacts == null || contacts.size() <= 0) return new String[0][];
String contactArray[][] = new String[contacts.size()][2];
for (int i = 0; i < contacts.size(); i++){
contactArray[i][0] = contacts.get(i).getName();
contactArray[i][1] = contacts.get(i).getNumber();
}
return contactArray;
}
private static void search(){
// do search stuff
JTextField textField = (JTextField) componentMap.get(SEARCH_FIELD);
String searchTerm = textField.getText();
List<Contact> contacts;
if (searchTerm != null && searchTerm.length() > 0){
contacts = phonebookService.findContactsByName(searchTerm);
contacts.addAll(phonebookService.findContactsByNumber(searchTerm));
} else {
contacts =phonebookService.getContacts();
}
defaultTableModel.setDataVector(useContactsForTable(contacts),new String[]{"Name",
"Number","Delete"});
JTable jTable = (JTable) componentMap.get(TABLE);
TableColumn delteCol = jTable.getColumn("Delete");
DeleteButton deleteButton = new DeleteButton("Delete");
deleteButton.addButtonListener(new DeleteButtonListener() {
@Override
public void buttonClicked(int row, int col) {
delete(row,col);
}
});
delteCol.setCellEditor(deleteButton);
delteCol.setCellRenderer(deleteButton);
defaultTableModel.fireTableDataChanged();
}
private static void addContact(){
JTextField addName = (JTextField) componentMap.get(ADD_NAME);
String nameString = addName.getText();
JTextField addNumber = (JTextField) componentMap.get(ADD_NUMBER);
String numberString = addNumber.getText();
Contact newContact = new Contact(nameString,numberString);
phonebookService.addContact(newContact);
JTable jTable = (JTable) componentMap.get(TABLE);
addName.setText("");
addNumber.setText("");
defaultTableModel.setDataVector(useContactsForTable(phonebookService.getContacts()),new String[]{"Name",
"Number","Delete"});
TableColumn delteCol = jTable.getColumn("Delete");
DeleteButton deleteButton = new DeleteButton("Delete");
deleteButton.addButtonListener(new DeleteButtonListener() {
@Override
public void buttonClicked(int row, int col) {
delete(row,col);
}
});
delteCol.setCellEditor(deleteButton);
delteCol.setCellRenderer(deleteButton);
defaultTableModel.fireTableDataChanged();
}
public static void delete (int row, int col){
System.out.println("delete "+ row+ " "+ col);
JTable jTable = (JTable) componentMap.get(TABLE);
String name = (String) defaultTableModel.getValueAt(row,0);
String number = (String) defaultTableModel.getValueAt(row,1);
Contact contactToDelete = new Contact(name,number);
phonebookService.deleteContact(contactToDelete);
defaultTableModel.setDataVector(useContactsForTable(phonebookService.getContacts()),new String[]{"Name",
"Number","Delete"});
TableColumn delteCol = jTable.getColumn("Delete");
DeleteButton deleteButton = new DeleteButton("Delete");
deleteButton.addButtonListener(new DeleteButtonListener() {
@Override
public void buttonClicked(int row, int col) {
delete(row,col);
}
});
delteCol.setCellEditor(deleteButton);
delteCol.setCellRenderer(deleteButton);
defaultTableModel.fireTableDataChanged();
}
}

The Main Method

Lets dive into the main method:

PhonebookGUI.java
public static void main(String[] args) {
PhonebookGUI phonebookGUI = new PhonebookGUI();
List<Contact> contactList = new ArrayList<>();
contactList.add(new Contact("John","123"));
contactList.add(new Contact("Moe","234"));
phonebookService = new PhonebookService(contactList);
phonebookGUI.setTitle(APP_NAME);
phonebookGUI.getContentPane().setLayout(new BoxLayout(phonebookGUI.getContentPane(),BoxLayout.PAGE_AXIS));
init(phonebookGUI, phonebookService);
phonebookGUI.setSize(PREF_WINDOW_WIDTH,PREF_WINDOW_HEIGHT);
phonebookGUI.setVisible(true);
}

On line two we create a new instance of our PhonebookGUI. The we create a new List of Contacts and add two dummy contacts, we will display them later on in our gui.

We use the method setTitle() to set the name of our app to a defined constant. Then we call setLayout to set a new BoxLayout Layout Manager.

Initializing our GUI Components

We use the init method to create our GUI components.

private static void init(Phonebook phonebook, PhonebookService phonebookService) {
JLabel jLabel = new JLabel(APP_NAME);
JPanel searchPanel = createSearchPanel();
JPanel addPanel = createAddPanel();
JScrollPane pane = createTablePane(phonebookService);
phonebook.add(jLabel);
phonebook.add(searchPanel);
phonebook.add(addPanel);
phonebook.add(pane);
for(Component c : searchPanel.getComponents()){
componentMap.put(c.getName(),c);
}
}

Listener Interface

DeleteButtonListener.java
package com.juniordev.phonebook.GUI;
import java.util.EventListener;
public interface DeleteButtonListener extends EventListener {
default void buttonClicked (int row , int col){
Phonebook.delete(row,col);
}
}

Delete Button

DeleteButton.java
package com.juniordev.phonebook.GUI;
import javax.swing.*;
import javax.swing.event.CellEditorListener;
import javax.swing.table.TableCellEditor;
import javax.swing.table.TableCellRenderer;
import java.awt.*;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.util.EventObject;
import java.util.Vector;
public class DeleteButton extends JButton implements TableCellRenderer, TableCellEditor {
private int selectedRow;
private int selectedColumn;
Vector<DeleteButtonListener> listener;
public DeleteButton(String text) {
super(text);
listener = new Vector<DeleteButtonListener>();
addActionListener(new ActionListener() {
public void actionPerformed( ActionEvent e ) {
for(DeleteButtonListener l : listener) {
l.buttonClicked(selectedRow, selectedColumn);
}
}
});
}
@Override
public Component getTableCellEditorComponent(JTable jTable, Object o, boolean b, int i, int i1) {
selectedRow = i;
selectedColumn = i1;
return this;
}
@Override
public Object getCellEditorValue() {
return "";
}
@Override
public boolean isCellEditable(EventObject eventObject) {
return true;
}
@Override
public boolean shouldSelectCell(EventObject eventObject) {
return true;
}
@Override
public boolean stopCellEditing() {
return true;
}
@Override
public void cancelCellEditing() {
}
@Override
public void addCellEditorListener(CellEditorListener cellEditorListener) {
}
@Override
public void removeCellEditorListener(CellEditorListener cellEditorListener) {
}
@Override
public Component getTableCellRendererComponent(JTable jTable, Object o, boolean b, boolean b1, int i, int i1) {
return this;
}
public void addButtonListener(DeleteButtonListener buttonListener){
this.listener.add(buttonListener);
}
public void removeButtonListener(DeleteButtonListener buttonListener){
this.listener.remove(buttonListener);
}
public Component getTableCellRendererComponent(JTable jTable, Object o, boolean isSelected, int row, int col) {
selectedRow = row;
selectedColumn = col;
return this;
}
}