Java programming standards¶
OTS is programmed in java. The strictly typed and object oriented nature provides the necessary tools for large scale software developed by a team. Java is multi-platform and offers automatic garbage collection (freeing up memory by discarding data that is no longer part of the active program). These features reduce the required effort to make OTS run on different computers. This section discusses some of the standards of how java is used in OTS. It is by no means a utility to learn java. In fact, a basic understanding of java is a prerequisite. Developers of OTS are advised to read ‘Effective Java’ by Joshua Bloch. It deepens a basic understanding of java by explaining good and bad design practices. Another source is the YouTube channel CodeAesthetic, a channel on good design practices, presented in a somewhat black-and-white fashion to challenge your habits.
Eclipse¶
Eclipse is an Integrated Development Environment (IDE), which is a tool that offers many features for software development. It is the advised tool to develop and use OTS. A full guide on installing and setting up Eclipse can be found here. Here, we describe some important components for OTS.
- Maven; Maven is a tool in which several java projects are coupled, and which determines how a project is built in to a stand-alone application. The several separate java projects in OTS, such as ots-core and ots-road, depend on one another as defined with Maven. This also determines which parts need to be included in a stand-alone compiled version of OTS. An overview of the most important project dependencies is given in Figure 2.2. The file central to Maven in each project is called pom.xml.
- JDK; the Java Development Kit is a low-level distribution for java development. It contains a Java Runtime Environment (JRE), which is required to run java. Additionally, it contains compilers to compile source code in to byte code, and other tools like Javadoc, a debugger and jvisualvm. The latter is a tool to evaluate CPU and memory usage of a java application.
- Checkstyle; checkstyle is a plugin for Eclipse which prescribes a consistent way of formatting code between developers of OTS. Think about line length, bracket placement, etc.
ots-demo | ots-editor demo's and user interface, this should be the starting point for getting to know OTS |
|||
ots-xml native xml import |
ots-swing java based animation |
||
ots-trafficcontrol event-based traffic control |
ots-animation animation functionality independent of implementation |
||
ots-road microscopic simulation of vehicular traffic |
ots-draw swing mimicking fuctionality |
||
ots-core core of traffic simulation including network representation and macroscopic models |
ots-kpi stand-alone key-performance-indicator module, including trajectory sampling |
||
ots-base contains some generic simulation utilities such as parameter management |
|||
ots the main project containing compilation standards, github workflows, documentation source code and general settings files |
Figure 2.2: OTS project dependencies. Projects depend on the projects directly below them.
Other dependencies are:
- ots-web web-based visualization
- ots-xml
- ots-animation
- ots-sim0mq-swing databus communication examples
- ots-sim0mq
- ots-swing
- ots-sim0mq-kpi databus communication kpi examples
- ots-sim0mq
- ots-sim0mq databus communication
- ots-xml
Commit checklist¶
The following list defines some checks that code has to meet in order to be eligible for inclusion in OTS.
- Checkstyle and code formatter; Have you considered all checkstyle warnings (and solved or knowingly suppressed them), and have you formatted/indented the code (Ctrl+Shift+F in Eclipse) according to the default OTS formatting? (The default OTS formatting is based on
dsol-checks.xml
in the main project under config.) - Compiler warnings and errors; Committing code with compiler errors should be avoided at all times. Warnings should be payed attention to.
- Override
toString()
; For most classes, thetoString()
method should be overridden. For purposes of debugging and understanding, objects should be able to meaningfully report what they are. In some cases, a superclass may have a sufficient implementation. - Override
equals()
?; The equals method should be implemented for all classes that are probable to be used for equality checks. The default java implementation checks for pointer equivalence, which can in many cases give a wrong result, especially in distributed serialization/deserialization or multiple JVM settings. Note that usage as key in aMap
meansequals()
will be used. - Then also override
hashCode()
; Whenever anequals()
method is defined, ahashCode()
method should also be defined, consistently with attributes considered inequals()
. - Implement
Comparable<Class>
?; For classes that are likely to be ordered, theComparable<Class>
interface should be implemented. - Exceptions in @throws; The javadoc should include
@throws
tags for allRuntimeException
s that it explicitly throws (i.e. for which the method containsthrow new ...
orThrow.when(...)
), and for all exceptions that are notRuntimeException
the method may throw itself or by using other code. The method signature should not include exceptions of typeRuntimeException
. - Pre- and post-conditions; Checking of input arguments should be done when it adds information relative to an exception occurring otherwise, and providing context from the contract of the method. Or when it prevents OTS from running without exception but incorrectly in terms of modeling. Input arguments should be checked where they matter, i.e. in the method where the problem occurs. Calling methods should not unnecessarily double check the arguments. For input checks OTS provides several easy and short
Throw.when(…)
orThrow.whenNull(…)
methods. Exceptions that are thrown should be java library exceptions when appropriate, such asIllegalArgumentException
. - Records; Use
record
for final objects when possible. Also return e.g. arecord MyOutput(double x, double y)
rather than adouble[]
with length 2. - Long methods; Prevent long methods (checkstyle gives a warning for methods longer than 150 lines). Refactor parts to helper methods with a clear name (in Eclipse, select relevant code block: refactor > extract method). In this way the algorithm a method performs also becomes more readable. If this is not possible without many input arguments, consider grouping input arguments in a small data class, possibly a
record
. - Unnecessary arguments; Avoid unnecessary input arguments that can be obtained through other input arguments. For example
Simulator sim, Lane lane
, while the simulator can be obtained withlane.getSimulator()
. - Var; For code readability and in case of verbose type definitions that are known from context, use
var
. For exampleStream<Entry<ParameterType<?>, Set<?>>> paramValues = map.entrySet().stream();
can be written asvar paramValues = map.entrySet().stream();
. - Inner classes; The above rules also hold for inner-classes. The Javadoc for internal classes can skip the copyright and authors.
When java library exceptions do not cover the exception well, OTS has a set of exceptions that can be thrown. These exceptions are:
OtsException
- XmlParserException
- SamplingException
- ParameterException
- NetworkException
- ProbabilityException
- TrafficControlException
- GtuException
- OperationalPlanException
- MissingComponentException
OtsRuntimeException
- OtsGeometryException
- CircularDependencyException
- CollisionException
Java generics¶
Java generics can initially seem hard and unwieldly, especially when classes are defined with multiple type arguments, which itself may also have type arguments. Generics however provides excellent ways in which code can be made as generic and re-usable as possible. Therefore, it is applied often in OTS and can be found in much of the source code. Hence, some understanding of generics is favorable. Note however that in case of good design, java generics are present in highly flexible low-level classes, while higher level classes that users mostly interact with hide much of the generics involved. This section is not meant to introduce the concept of java generics. Information to learn about java generics can easily be found elsewhere. Here a basic understanding of java generics is assumed, to further explain some design patterns with java generics as used in OTS.
Below is an example of recursive generics, where HierarchicalType
has type parameter T
, which is defined to be a subclass of HierarchicalType
itself. This pattern is useful when a subclass implementation needs to accept or return objects of its own type, while we want to define the functionality only once. For instance, we have GtuType
which is a subclass of HierarchicalType
. Without re-specifying the getParent()
method, it is guaranteed that for a GtuType
, it will return a GtuType
. This holds for any subclass, and the pattern allows us to define the functionality once.
public abstract class HierarchicalType<T extends HierarchicalType<T>>
implements Type<T>
{
private T parent;
public final T getParent()
{
return this.parent;
}
}
public final class GtuType extends HierarchicalType<GtuType>
{
}
The next example shows how one object can house multiple objects of the same class, that have different type parameters, but offer no difficulty to the outside user. We do this by applying safe casting within a limited scope which guarantees the cast is safe. In this case we have ParameterSet
, which has multiple parameters, each having a certain type for the value. The method setParameter(…)
assures that for a set parameter value, the value type matches the parameter type T
(which may differ between different calls of the method). To store the values, there is a map between parameters of any type <?>
, to simply an object of any type. The getParameter(…)
method performs a cast of the value obtained from the internal map. Given this context, where only the method setParameter(…)
has write access to the Map
, we are certain the value object matches the parameter type, and the cast is safe. Hence we can suppress the warning.
public class ParameterSet
{
private Map<ParameterType<?>, Object> parameters;
public final <T> void setParameter(final ParameterType<T> parameterType,
final T value)
{
this.parameters.put(parameterType, value);
}
@SuppressWarnings("unchecked")
public <T> T getParameter(final ParameterType<T> parameterType)
{
return (T) this.parameters.get(parameterType);
}
}
Next, we give an example of super generics, which are often considered vague, but make sense in an actual use of the super keyword with java generics. In this example we discuss a ParameterType<T>
as in the previous example. Any parameter type may be defined with some constraint, for example that the value has to be above 0. We may have many parameter types which would have such a constraint, but have different type arguments. For example, parameter types using Length
, Duration
, Acceleration
, etc. Since all these types are a subclass of Number
, returning the SI value as number, it would be convenient to have one single Constraint<Number>
and use this on all mentioned parameter types. This is possible by letting the parameter type implementations accept a constraint typed with the parameter value type or a super class of it. For example, ParamaterTypeLength
which is typed with Length
, can receive any constraint that can operate on a Length
or any of its super classes. A constraint on Number
, or Object
, would thus be acceptable, as Length
is both a Number
and an Object
.
public class ParameterTypeLength extends ParameterTypeNumeric<Length>
{
private Constraint<? super Length>
}
Next, we show an example using quite some type arguments, to show that although at first glance this may seem unclear, it makes perfect sense. The example discusses a PerceptionCollectable
. It is designed to allow iteration over perceived representations of type H
regarding underlying objects in simulation of type U
. Furthermore, it can perceive a collected result of type C
, resulting from considering all objects of type U
together. Finally, as the collected (accumulated) result is determined, there is an intermediate result of type I
. As a concrete example, density may be determined by considering leaders of type U = Gtu
, perceived as H = HeadwayGtu
, resulting in a density C = LinearDensity
, with an intermediate type I
which is some class that stores a cumulative GTU count, and the distance over which these GTUs are found. The identity returns an initial value for this (count = 0), while the accumulator increases the result for every next GTU, and the finalizer translates the last intermediate result in a density. We again see the super
keyword, as for instance we could have a PerceptionCollectable
of lane-based GTUs with U = LaneBasedGtu
. From the set of lane-based GTUs we could use an accumulator of GTUs (so a superclass of LaneBasedGtu
) as for instance only speed is used, which GTUs also have. Without the super keyword, a PerceptionAccumulator<Gtu, ?>
could not be used.
public interface PerceptionCollectable<H extends Headway, U>
extends PerceptionIterable<H>
{
<C, I> C collect(Supplier<I> identity,
PerceptionAccumulator<? super U, I> accumulator,
Function<I, C> finalizer);
}
At the beginning of this section it was mentioned that good design should hide much of the underlying java generics being used in lower level classes. Though this is true, it doesn’t excuse the developer of low-level functionality from using comprehensive java generics. As an example of how comprehensive java generics can be hidden, consider the class Length
. Below the definition of its 2nd comprehensive super class is shown. Users of Length
have to define no type argument, but lower level classes make sure that correct calculations are made (e.g. adding Length
and not Speed
) and only units pertaining to the length quantity are used.
public abstract class DoubleScalarRelWithAbs<AU extends AbsoluteLinearUnit<AU, RU>,
A extends DoubleScalarAbs<AU, A, RU, R>, RU extends Unit<RU>,
R extends DoubleScalarRelWithAbs<AU, A, RU, R>> extends DoubleScalarRel<RU, R>
implements RelWithAbs<AU, A, RU, R>
These are just a few examples of how java generics is used in OTS. If done well, it’s only a ‘relative headache’ at one location, while providing high flexibility, reusability and cleaner and more intuitive code elsewhere.
JUnit tests¶
JUnit is a framework that is used in Eclipse to perform ‘unit tests’ for OTS. A unit test is code, outside of the functional code of a project, that tests functionality of the functional code at a unit level. A typical example is to invoke a method with prescribed input, and checking whether the method output is as expected. This is then repeated for a range of input values. As such the unit test verifies that the method functions as it should. Ideally every part of functional code is subject to a unit test such that proper functioning of OTS is guaranteed. Even still it does not exclude bugs that arise from complex interactions between methods or regarding unforeseen cases. Nonetheless, unit tests help to keep a complex project robust and bug free. A tutorial in section 8 discusses how JUnit tests are created.