CPU Load Bug: Wrong Usage of Timer
and repaint()
repaint()
calls paintComponent()
. The way your code is written, your Java program is repainting all the time, consuming a CPU core completely. Instead it should repaint only when a change happens, and that is if the user changed the color or the timer was triggered. The call to repaint()
should not be in paintComponent()
but in actionPerformed()
.
It also means that the call to date()
is in the wrong method. While it works being in paintComponent()
, it should actually be in actionPerformed()
- the timestamp shall be updated when the timer expires, not when the paint happens, because painting might happen much more frequently.
Remove unused symbols
Your fields button
and time
are not used and their initializations involve no (relevant) side-effects, you can remove them completely.
Fields should be private
All your fields are public
. Fields should be private
. Technically you can make public
fields, but that's considered bad design: It violates data hiding, one of the fundamental principles of object-oriented design. The consequence would be a breach of encapsulation and cohesion with too tight coupling to internals.
Prefer delegation over inheritance (and follow LSP)
It's often misunderstood that inheritance is at the core of OOP. It's not. What's at the core of OOP is polymorphic abstraction. Inheritance is the cream on top of it, and just as too much cream isn't healthy for the body, too much inheritance is not healthy for a design.
Using inheritance can also lead to a violation of the LSP - Liskov Substitution Principle. Subclasses are to be able to replace their superclasses in all cases. But your Clock
class cannot be used or extended by yet another class to get an even more specialized version of a Window
(or Frame
or JFrame
) in a meaningful way.
Another sign of wrong inheritance is that your class Clock
does not add any behavior to its base class JFrame
.
So, instead, you should not inherit from JFrame
but just use a JFrame
in your main method.
Also, pick the right superclass: Use JComponent
instead of JPanel
.
Your ScreenSaver is actually not a JPanel. A JPanel is a component designed to be a container with a layout for reuse. What you want as superclass is JComponent. You have multiple components, and that's possible with JComponent, but you don't want anyone to take your ScreenSaver and mess around with its LayoutManager and its contained components.
Think of data structures for replacing switch case
- OCP
switch case
you might ask? You have a long chain of if else
which is just a switch case
in disguise. In this case, it could easily be replaced by a Map<String, Color>
. You could even use that map to initialize the JComboBox
.
This is also related to a principle known as OCP - Open Closed Principle. It was first described by Bertrand Meyer and is one of the first five principles of object-oriented class design collected by Robert C. Martin. It says that, when a new feature needs to be implemented, a class (or actually any software entity on its level of abstraction) should be open for extension but closed for modification. It can also be seen as a special case of the SRP - Single Responsibility Principle: a class (or actually any software entity on its level of abstraction) should have only one reason to change. Which is interesting, as in a side track that defines responsibility as reason to change.
Now what is that theoratical babble all about? Imagine you want to add a new color: In your program you have to make a change in two (actually three) places:
- The list of items given to the
JComboBox
,
- and the color names
actionPerformed()
method that selects the color,
- and the actual colors in the
actionPerformed()
method.
That can easily get out of sync. But even worse: There could also be more subtile errors, like in the JComboBox
grey being written as "Grey", and in the actionPerformed()
method grey being written as "Gray". Such errors can be very difficult to spot.
When you use a single central Map<String, Color>
, then in your system there will be only one authority over the information of colors and their display names. Nothing can get out of sync. Much easier to maintain, much less error prone. You could even later easily load this stuff from a file then.
Use verbs for method names
Your method date
is a noun. Okay, it can be argued that date
actually works very very well as a verb, too. Just maybe not in this context, in this context date
is thought of as a noun. So you could use updateDate
instead.
Avoid mutable static fields
Mutable static fields are a dangerous source of error. They prevent multiple instances of the class to work properly independent of each other.
You might argue that in this case, there would never be two instances with different values for date
and df
. It would still be wrong, for two reasons:
You might want to run two instances in the same VM on different clocks (see below for Java 8 datetime API), for example to show different time zones, or just for testing. Then sharing the data between the instances would be wrong.
While the fields are global (once), there still are multiple Timer
s (one for each instance of Clock
).
Reduce scope as much as possible - use local variables.
The variables panel
, date
and df
do not need to be global, you can turn them into local variables of the respective methods.
Put the right amount of code inside the try
-Block.
That you initialize font
with null
, then catch exceptions, which means font
might still be null
, and then continue using font
afterwards, means that the remaining code eventually uses null
. null
should be avoided as much as possible, and those situations where null
cannot be avoided, should be contained and controlled as much as possible.
Load and Initialize Only Once.
You do not need to load the font and register it with the graphics environment every time the component is repainted. It's sufficient to load and register the font once during application startup.
Similarly you do not need to create a new DateFormat
every time the Timer
calls your actionPerformed()
method. It's sufficient to create only one global DateFormat
object.
Collapse catch blocks
You can use Java 7 multicatch to collapse multiple catch blocks into one when the code executed for the catch blocks is identical.
try {
// some code
} catch (Exception1 e) {
e.printStackTrace();
} catch (Exception2 e) {
e.printStackTrace();
}
can be replaced with
try {
// some code
} catch (Exception1 | Exception2 e) {
e.printStackTrace();
}
Make smaller methods
Your methods are quite big. Ideal methods do only one thing, they do it well and they do it only. For example, loading the font (including registering) is one thing, handling the exceptions in case the font couldn't be loaded, and when it was loaded successfully, setting it for the component, is another thing.
Make the change of color immediate
It feels a bit weird for a user to change the color and not seeing anything happening (until the second expires). For me, a second is a pretty long time. I can type 10 characters in a second, or shoot 3 people in Unreal Tournament when I'm lucky. You could change your program in a way that changes to the color become immediate.
Let Swing keep track of what it can
Swing can keep track of a lot of stuff. Explore the API. Your ScreenSaver is a JPanel
. A JPanel
is a JComponent
. As such, it has font, foreground color and background color. You can just set the font and the color, and the Graphics
object for the paintComponent()
method will be initialized accordingly.
You might also want to consider simply involving a JLabel
.
Add @Override
annotation when implementing or overriding methods
This way the tools can help you when you misspell your method name and for example override paintcomponent(Graphics g)
instead of paintComponent(Graphics g)
.
Consider import static
Often, it's plain obvious where static methods and constants are coming from. For example, in your context, it's plain obvious that RED
is coming from class Color
. In cases where it is plain obvious, import static
can help making the source code more terse without losing clarity.
The Timer
should only be running while the component is visible
So you should have a ComponentListener
which starts the timer in componentShown()
and stops the timer in componentHidden()
.
Refer to static symbols via the declaring class.
It's better to refer to symbols via the declaring class and not via any of the subclasses. For example, EXIT_ON_CLOSE
is declared in interface WindowConstants
. So it should be referred to as WindowConstants.EXIT_ON_CLOSE
and not JFrame.EXIT_ON_CLOSE
.
Prefer DISPOSE_ON_CLOSE
over EXIT_ON_CLOSE
.
It's better to use DISPOSE_ON_CLOSE
then EXIT_ON_CLOSE
. Yes, it is harder to program correctly with DISPOSE_ON_CLOSE
, but that's exactly the intention: When your application doesn't properly stop and exit with DISPOSE_ON_CLOSE
, there's a flaw (usually in the threading model). Using EXIT_ON_CLOSE
instead of DISPOSE_ON_CLOSE
just bypasses that flaw. That can even be error-prone if there is another thread in the application that's still busy doing important stuff (like saving a user's file). That thread would be terminated in the middle of its work.
Prefer main(String... args)
over main(String[] args)
That's not really big and important. It's just easier for testing:
- With
main(String[] args)
a no args test-call is main(new String[0])
.
- With
main(String... args)
a no args test-call is simply main()
.
Consider using final
for variables as much as possible
It helps reducing programming mistakes and drives you towards the functional programming paradigm.
Split ScreenSaver in 2-3 classes
Actually ScreenSaver
is doing more than one thing. It's displaying the date in a color of the user's liking, and it's enabling the user to pick the color of their liking. While initially it requires some thought of how to split this, in the end the code will become simpler if you achieve this.
Names: Is ScreenSaver
really a screen saver? And is the Clock
really a Clock?
Also, I would avoid using names that are in the API already, and there already is a class called Clock
in the API. It isn't always possible (and sometimes not even intended) to avoid name-clashes, obviously. Just a thought.
Use Java 8 Date API
The classes Date
, Calendar
and such are actually horrible, and code that uses them is difficult to test.
You may consider using the new Java 8 API for datetimes, java.time
.
It is really awesome.
Calling deriveFont()
the right way
In case your font couldn't be loaded, the default font will have the default size instead of 30pt.
I think that's not intended.
It's better to call setFont(...)
and then derive the font by setting it again using setFont(getFont().deriveFont(30f))
.
Use the correct thread for the job
The setup of your application runs in the main thread. Ideally, modifications (and even reads) on Swing components happen only on the AWT Event Thread, unless the method that's called is explicitely documented as being thread-safe.
In your case it will probably not matter. But it's still safer and best practice to even create the UI not in the main thread but on the AWT Event Thread. The class SwingUtilities
provides the methods invokeAndWait()
and invokeLater()
for that.
Your main()
method could look like this:
public static void main(String... args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame();
frame.setContentPane(new ScreenSaver());
frame.setSize(685, 80);
frame.setUndecorated(true);
frame.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
frame.setBackground(Color.BLACK);
frame.setResizable(false);
frame.setVisible(true);
frame.toFront();
});
}
Or, putting the code for setting up the UI in a separate method, like this:
public static void main(String... args) {
SwingUtilities.invokeLater(Clock::setupUI);
}
private static void setupUI() {
JFrame frame = new JFrame();
frame.setContentPane(new ScreenSaver());
frame.setSize(685, 80);
frame.setUndecorated(true);
frame.setDefaultCloseOperation(DISPOSE_ON_CLOSE);
frame.setBackground(Color.BLACK);
frame.setResizable(false);
frame.setVisible(true);
frame.toFront();
}
Last but not least
Consider testing your application.
The most popular test frameworks (test runners) for Java probably are JUnit, Cucumber and TestNG.