I hope you don't mind that I mostly copy&pasted my last answer to one of your questions, but basically, the points are mostly still the same.
Whitespace
There should be a blank line between two method definitions.
Also, some blank lines inside of the methods would give the code room to breathe, and allow you to visually separate individual "steps" from each other, for example in your main
method:
int side = Integer.parseInt(args[0]);
int iterations = Integer.parseInt(args[1]);
Picture picture = new Picture(side, side);
paint(picture, iterations);
picture.show();
Formatting
The Java community has a fairly standard formatting style. In the early years of Java, Sun used to publish a standardized coding style. They stopped at some point, because they felt that it wasn't the job of the language creator to tell people how to write their code, but most of the community still sticks to those rules.
Another popular style guide is the Google Java Style Guide, which builds on Sun's, but deviates in a couple of places, e.g. Sun's style guide specified an indentation of 4 columns to be achieved either with spaces or tabs, Google specifies 2 spaces.
Most of these style guides would suggest to leave whitespace between method definitions. Also, almost all Java style guides use some form of Egyptian Braces where the opening brace is on the same line as the declaration or keyword that starts the compound statement.
Seeing something like
private static Complex polynomial(Complex complexNumber)
{
Complex minusOne = new Complex(-1.0 ,0.0);
return complexNumber.times(complexNumber).times(complexNumber).times(complexNumber).plus(minusOne);
}
will look weird to a Java programmer. They would expect to see either
private static Complex polynomial(Complex complexNumber) {
Complex minusOne = new Complex(-1.0 ,0.0);
return complexNumber.times(complexNumber).times(complexNumber).times(complexNumber).plus(minusOne);
}
or
private static Complex polynomial(Complex complexNumber) {
Complex minusOne = new Complex(-1.0 ,0.0);
return complexNumber.times(complexNumber).times(complexNumber).times(complexNumber).plus(minusOne);
}
And something like this:
public Complex(double real, double imag)
{ re = real; im = imag; }
public double re()
{ return re; }
public double im()
{ return im; }
public double abs()
{ return Math.sqrt(re*re + im*im); }
is completely non-standard and non-idiomatic. It doesn't even match with the rest of your code. It should at least look like this to be consistent with the rest of your code:
public Complex(double real, double imag)
{
re = real;
im = imag;
}
public double re()
{
return re;
}
public double im()
{
return im;
}
public double abs()
{
return Math.sqrt(re*re + im*im);
}
but most Java style guides would prefer this:
public Complex(double real, double imag) {
re = real;
im = imag;
}
public double re() {
return re;
}
public double im() {
return im;
}
public double abs() {
return Math.sqrt(re*re + im*im);
}
Personally, I would also be fine with this:
public Complex(double real, double imag) {
re = real;
im = imag;
}
public double re() { return re; }
public double im() { return im; }
public double abs() { return Math.sqrt(re*re + im*im); }
And even this:
public Complex(double real, double imag) {
re = real;
im = imag;
}
public double re() { return re; }
public double im() { return im; }
public double abs() { return Math.sqrt(re*re + im*im); }
Personally, what I do is I set my editor to "auto-format while type", "auto-format on paste", and "auto-format on save", and then I can just turn off my brain and never need to think about formatting ever again.
Magic values
There are some magic values in your code, for example (255, 0, 0)
or (255, 255, 255)
.
Variables allow you to give explanatory, intention-revealing names to your values.
final var red = new Color(255, 0, 0);
However, note that as mentioned in Doi9t's answer, there is no need to define those yourself, all of the colors you are using are already predefined in Java SE.
Type inference
I am a big fan of type inference, especially in cases where the type is obvious:
int side = Integer.parseInt(args[0]);
How often do I have to told that this is an integer? I get it!
var side = Integer.parseInt(args[0]);
Same here:
Picture picture = new Picture(side, side);
This almost sounds like someone stuttering. I don't need the picture
variable to be explicitly annotated with the Picture
type to understand that a variable called picture
being initialized with a Picture
is probably a picture:
var picture = new Picture(side, side);
final
I am also a big fan of making everything that can be made final
explicitly final
. And even for things that can't be made final
as written, I'd investigate whether it can be rewritten so it can be made final
.
Note, by "everything" I mean primarily variables and fields. However, unless a class is explicitly designed to be extended, it should also be marked final
. And of course, immutable classes need to be final
anyway.
Naming
complexNumber
is not a very intention-revealing name. I already know it is a complex number from its type, but what does it do? What is its meaning?
Also note that Java SE does contain a couple of numeric classes such as BigInteger
and BigDecimal
, and they use the terms add
and multiply
instead of plus
and times
, so it would probably be a good idea to follow them.
polynomial
was already mentioned in another answer.
Code Duplication
There are two identical definitions of minusOne
. Also, the number -1
is used a third time in the paint
method, but in a hidden way.
You can pull these out into private static final
fields in your class. But really, if you compare the Complex
class to other numeric classes in the Java SE standard library, e.g. java.math.BigInteger
or java.math.BigDecimal
, or third-party classes from the larger Java community, e.g. Apache Commons Math's apache.commons.math4.fraction.Fraction
and JScience's org.jscience.mathematics.number.Rational
, and especially other complex number implementations such as apache.commons.math4.complex.Complex
or org.jscience.mathematics.number.Complex
, you will see that they expose important and often needed numbers as static
fields on the class itself.
Correctness
Your Complex
class is missing an implementation of equals
. As a result, it inherits the default implementation of equals
which checks for reference equality, which means that e.g. the following will be false
:
var two = new Complex(2d, 0d);
var alsoTwo = new Complex(2d, 0d);
System.out.println(two.equals(alsoTwo));
System.out.println(two.equals(Complex.ONE.add(Complex.ONE)));
You should pretty much always override equals
(and hashCode
), at least in all your data classes and model classes. For Complex
, it should be something like this:
@Override
public boolean equals(final Object other) {
if (other == null) {
return false;
}
if (this == other) {
return true;
}
if (other instanceof Complex) {
var otherComplex = (Complex) other;
return re() == otherComplex.re() && im() == otherComplex.im();
}
return false;
}
Clarity
In the pickColor
method, you sometimes use the order re
first, im
second, and sometimes the other way around. That is very confusing. At first glance, it looks like the first and third condition and the second and fourth condition are identical.
You should not switch the order in the middle of the method, and at least be consistent within the method but even better in the entire code. Also, try to be consistent with established conventions, and typically, the real part always comes first.
However, once you implement a proper Complex.equals
method, that problem will go away, because you can then simply compare complex numbers instead of having to pick them apart. And if you also implement the idea of giving them well-known names, the method will look like this:
if (complexNumber.equals(Complex.ONE)) {
return Color.RED;
} else if (complexNumber.equals(Complex.MINUS_ONE)) {
return Color.GREEN;
} else if (complexNumber.equals(Complex.I)) {
return Color.BLUE;
} else if (complexNumber.equals(Complex.MINUS_I)) {
return Color.WHITE;
} else {
return Color.BLACK;
}
Records
Records are an experimental feature in Java 14. "Experimental" means that they can be removed at any time without warning, changed in a backwards-incompatible breaking manner without warning, and require specific command line flags to work. So, they should not be used in production.
However, they are extremely useful, and classes like your Complex
class are exactly what they were made for. Changing your Complex
class into a record
reduces the code from this:
public final class Complex {
public static Complex ZERO = new Complex(0.0, 0.0);
public static Complex ONE = new Complex(1.0, 0.0);
public static Complex MINUS_ONE = new Complex(-1.0, 0.0);
public static Complex I = new Complex(0.0, 1.0);
public static Complex MINUS_I = new Complex(0.0, -1.0);
private final double re;
private final double im;
public Complex(final double re, final double im) {
this.re = re;
this.im = im;
}
public double getRe() { return re; }
public double getIm() { return im; }
@Override
public boolean equals(final Object other) {
if (other == null) {
return false;
}
if (this == other) {
return true;
}
if (other instanceof Complex) {
var otherComplex = (Complex) other;
return getRe() == otherComplex.getRe() && getIm() == otherComplex.getIm();
}
return false;
}
public double abs() {
return Math.sqrt(getRe() * getRe() + getIm() * getIm());
}
public Complex add(final Complex b) {
final var real = getRe() + b.getRe();
final var imag = getIm() + b.getIm();
return new Complex(real, imag);
}
public Complex multiply(final Complex b) {
final var real = getRe() * b.getRe() - getIm() * b.getIm();
final var imag = getRe() * b.getIm() + getIm() * b.getRe();
return new Complex(real, imag);
}
public Complex divide(final Complex b) {
final var real = (getRe() * b.getRe() + getIm() * b.getIm()) / (b.getRe() * b.getRe() + b.getIm() * b.getIm());
final var imag = (getIm() * b.getRe() - getRe() * b.getIm()) / (b.getRe() * b.getRe() + b.getIm() * b.getIm());
return new Complex(real, imag);
}
public String toString() { return getRe() + " + " + getIm() + "i"; }
public static void main(final String[] args) {
final var z0 = new Complex(1.0, 1.0);
var z = z0;
z = z.multiply(z).add(z0);
z = z.multiply(z).add(z0);
System.out.println(z);
}
}
to this:
public record Complex(final double re, final double im) {
public static Complex ZERO = new Complex(0.0, 0.0);
public static Complex ONE = new Complex(1.0, 0.0);
public static Complex MINUS_ONE = new Complex(-1.0, 0.0);
public static Complex I = new Complex(0.0, 1.0);
public static Complex MINUS_I = new Complex(0.0, -1.0);
public double abs() {
return Math.sqrt(re * re + im * im);
}
public Complex add(final Complex b) {
final var real = re + b.re;
final var imag = im + b.im;
return new Complex(real, imag);
}
public Complex multiply(final Complex b) {
final var real = re * b.re - im * b.im;
final var imag = re * b.im + im * b.re;
return new Complex(real, imag);
}
public Complex divide(final Complex b) {
final var real = (re * b.re + im * b.im) / (b.re * b.re + b.im * b.im);
final var imag = (im * b.re - re * b.im) / (b.re * b.re + b.im * b.im);
return new Complex(real, imag);
}
public String toString() { return re + " + " + im + "i"; }
public static void main(final String[] args) {
final var z0 = new Complex(1.0, 1.0);
var z = z0;
z = z.multiply(z).add(z0);
z = z.multiply(z).add(z0);
System.out.println(z);
}
}
We get the fields, the constructor, as well as correct implementations of equals
(I am not 100% convinced that my implementation is correct!) and hashCode
for free. We also get a sensible implementation of toString
, although we override it here to get the usual a + bi
representation.
Single Responsibility Principle
Your NewtonianChaos
class does two completely different things: it computes stuff and it draws stuff. One part is all about numbers. The other part is all about colors and coordinates.
Those are different responsibilities. They should be separated into different objects.
Likewise, your Complex
class is a pure data class. Why does it have a main
method? Because it does two things: it represents a complex number and it tests itself. Tests should go into … well … tests, not main
methods.
Testability, Modularity, Overall Design
- All your methods are
static
, in other words, they aren't really methods at all, they are glorified procedures. There are no objects!
- Your
NewtonianChaos
class is intricately linked to the Picture
class, there is no way to separate them, to use them independently, to test them independently.
It is hard to test code if I can't instantiate an object that I can test. It is hard to test code if I always need the entire thing and can't test pieces independently or swap out pieces for test versions.
At the moment, the only way to test your code, is to run the entire thing, take a screenshot and compare it with a pre-recorded one. This adds a huge amount of complexity to the tests, and is very slow. Ideally, you want to be able to run your tests every couple of seconds.
And actually, that only applies to you! I cannot even do that, because I don't have the Picture
class.
NewtonianChaos
should be a true object, not a static class. In fact, as mentioned above, it should actually be two objects. It should have the dependency on Picture
injected, not hard-coded. The Picture
dependency should be abstracted behind an interface
, so that, for testing purposes, I can replace the picture with a version that records the commands and compares them to a pre-recorded sequence instead of drawing on the screen.
import java.awt.Color;
public class NewtonianChaos {
private static Complex polynomial(final Complex complexNumber) {
return complexNumber.multiply(complexNumber).multiply(complexNumber).multiply(complexNumber)
.add(Complex.MINUS_ONE);
}
private static Complex polynomialDerivative(final Complex complexNumber) {
final var four = new Complex(4.0, 0.0);
return complexNumber.multiply(complexNumber).multiply(complexNumber).multiply(four);
}
private static Complex applyNewtonMethod(final Complex complexNumber, final int iterations) {
var complexRoot = complexNumber;
for (var i = 0; i < iterations; i++) {
final var newComplexRoot = complexRoot.add(
(polynomial(complexRoot).divide(polynomialDerivative(complexRoot))).multiply(Complex.MINUS_ONE));
if (newComplexRoot == complexRoot) { break; }
complexRoot = newComplexRoot;
}
return complexRoot;
}
private static Color pickColor(final Complex complexNumber) {
if (complexNumber.equals(Complex.ONE)) {
return Color.RED;
} else if (complexNumber.equals(Complex.MINUS_ONE)) {
return Color.GREEN;
} else if (complexNumber.equals(Complex.I)) {
return Color.BLUE;
} else if (complexNumber.equals(Complex.MINUS_I)) {
return Color.WHITE;
} else {
return Color.BLACK;
}
}
private static Picture paint(final Picture picture, final int iterations) {
final int width = picture.width();
final int height = picture.height();
for (int j = 0; j < width; j++) {
final double realPart = 1.0 * j / width;
for (int i = 0; i < height; i++) {
final double imaginaryPart = 1.0 * i / height;
final Complex complexNumber = new Complex(realPart, imaginaryPart);
final Complex complexRoot = applyNewtonMethod(complexNumber, iterations);
final Color color = pickColor(complexRoot);
picture.set(j, i, color);
}
}
return picture;
}
public static void main(final String[] args) {
final int side = Integer.parseInt(args[0]);
final int iterations = Integer.parseInt(args[1]);
final Picture picture = new Picture(side, side);
paint(picture, iterations);
picture.show();
}
}
public record Complex(final double re, final double im) {
public static Complex ZERO = new Complex(0.0, 0.0);
public static Complex ONE = new Complex(1.0, 0.0);
public static Complex MINUS_ONE = new Complex(-1.0, 0.0);
public static Complex I = new Complex(0.0, 1.0);
public static Complex MINUS_I = new Complex(0.0, -1.0);
public double abs() {
return Math.sqrt(re * re + im * im);
}
public Complex add(final Complex b) {
final var real = re + b.re;
final var imag = im + b.im;
return new Complex(real, imag);
}
public Complex multiply(final Complex b) {
final var real = re * b.re - im * b.im;
final var imag = re * b.im + im * b.re;
return new Complex(real, imag);
}
public Complex divide(final Complex b) {
final var v = b.re * b.re + b.im * b.im;
final var real = (re * b.re + im * b.im) / v;
final var imag = (im * b.re - re * b.im) / v;
return new Complex(real, imag);
}
public String toString() {
return re + " + " + im + "i";
}
public static void main(final String[] args) {
final var z0 = new Complex(1.0, 1.0);
var z = z0;
z = z.multiply(z).add(z0);
z = z.multiply(z).add(z0);
System.out.println(z);
}
}
```
double realPart = 1.0 * j / width;
doesn’t need to be in the innermost loop wherej
doesn’t change in the iterations. Generally, you could make the expensive calculations1.0/width
and1.0/height
at the beginning of the method and only multiplyi
andj
with these factors in the loop. \$\endgroup\$pickColor()
, you assume that you exactly reach one of the four root values. And luckily you do quite often, as we see in the picture. But with floating point computations, I'd allow for the typical rounding errors, so e.g. check whether the result lies within +/- 1.0E-10 of the target value. \$\endgroup\$applyNewtonMethod()
. The computation will quite often reach its final value before the iterations are exhausted. You can leave the loop whenever the currentcomplexRoot
is exactly equal to the previous iteration's value. From that point on, all future iterations will only repeat exactly the same computation. \$\endgroup\$