Blog-Archiv

Sonntag, 20. Dezember 2020

Why Delegation is Not Better than Inheritance

The object-oriented idea often is associated with inheritance and obscure hierarchies. Lots of books and web articles recommend to prefer delegation to inheritance. Do they implicitly recommend to not use object-oriented languages?

In this Blog I want to bring more light into the discussion about "inheritance versus delegation", which I consider unbalanced. Mind that, from a technical point of view, both delegation and inheritance are about code reusage. Nevertheless, from the business perspective, inheritance may represent a determining and deliberate concept that is nice to have also in source code.

(Table of contents generated by JavaScript.)

Code Examples

We are going to implement following application by both inheritance and delegation:

public class Main
{
    public static void main(String[] args)  {
        Dog lupo = new Dog();
        lupo.eat("sausage");
        lupo.bark();

        Cat garfield = new Cat();
        garfield.eat("milk");
        garfield.miaow();
    }
}

Both the dog lupo and the cat garfield can eat, but only the dog can bark, and only the cat can miaow. We want to put the reusable eat() logic into a common class, first using inheritance, then delegation.

Inheritance

Inheritance represents the "IS A" concept:

  • A cat is an animal

This is categorization, one of the oldest scientific ways of working, closely related to classification, generalization, abstraction.

class Animal
{
    private String eatenFood;
    
    public void eat(String food)   {
        this.eatenFood = food;
    }

    public String getEatenFood() {
        return eatenFood;
    }
}

class Dog extends Animal
{
    public void bark() {
        System.out.println("Wow Wow");
    }
}

class Cat extends Animal
{
    public void miaow()  {
        System.out.println("Miaaaaaow");
    }
}

Both Dog and Cat inherit from Animal, and thus can eat(). A Dog has the special ability to bark(), a Cat the ability to miaow(). This code is short and concise and contains no duplication of any kind.

Delegation

Delegation can represent both the "HAS A" and the "IS A" concept:

  • A cat has an animal (inside, thus it is an animal)

Doesn't sound very intuitive, but is acceptable from a technical perspective, because inheritance is resolved to delegation by the compiler!

class Animal
{
    private String eatenFood;
    
    public void eat(String food)    {
        this.eatenFood = food;
    }
    
    public String getEatenFood()    {
        return eatenFood;
    }
}

class Dog
{
    private Animal animal = new Animal();
    
    public void eat(String food) {
        animal.eat(food);
    }
    
    public String getEatenFood()    {
        return animal.getEatenFood();
    }
    
    public void bark() {
        System.out.println("Wow Wow");
    }
}

class Cat
{
    private Animal animal = new Animal();
    
    public void eat(String food) {
        animal.eat(food);
    }
    
    public String getEatenFood()    {
        return animal.getEatenFood();
    }
    
    public void miaow()  {
        System.out.println("Miaaaaaow");
    }
}

Both Dog and Cat have an Animal within them, but calls to eat() and getEatenFood() have to be forwarded to that delegation object. This is purely technical code that duplicates Animal method signatures. The specific abilities like bark() and miaow() can hardly be seen, because all the delegate methods are public. (Yes, this clutters code!)

When to Use What

Use delegation when having a clear containment hierarchy like "A cat has a tail".

Deriving a LabeledButton from HorizontalLayout just to be able to use the layout methods without having to create a delegate is not worth it. You would expose the HorizontalLayout API to all callers of LabeledButton. Moreover the next release may demand the layout to be switchable to VerticalLayout at runtime.

Only inherit from a class that you want 100% inside your class. There should not be a single public or protected super-method that doesn't fit to your class.

A Stack with push(), pop(), peek() is not expected to inherit from List with add(), remove(), contains(), as such an API would look absurd for a Stack.
Why extend Thread when you just want some logic to be runnable in a separate thread? Most likely you won't even use the inherited Thread methods in your logic. Better implement Runnable and let the caller decide about a thread of its choice.
UI controllers (callback method containers) are a border case. Practice shows that here inheritance leads to very high and incomprehensible hierarchies that end up to be mixins actually.

When you need to reuse more than one class, use delegation, even if one of the super-classes would fit 100% to your class.

When you need to inherit dynamically at runtime, you need to use delegation, because inheritance is compile-time bound.

Use inheritance in any other case, especially when having a clear inheritance hierarchy like "A cat is a mammal". Inheritance is a much better reusage concept than delegation.

We still live in times of manually written source code, and very frequently the source code is the only representation of a business logic that came from experts that are long gone and left no documentation ("we are agile"). Thus we need well readable und quickly understandable source code without duplications, easily maintainable, because maintenance makes up 70% of software production efforts.

Once again I want to remark that inheritance is resolved to delegation at compile time, so why implement delegation when the compiler can do it for you, without typos and code duplication?

Advantages and Disadvantages

Mind that any such list always is driven by the point of view: A compiler engineer may give other pros and cons than a database expert. I am a business developer.

Inheritance

Advantages

  • You effortlessly get whatever the super-class provides, without any code duplication
  • You could fix bugs or customize behavior in a class you have no editing-access to (external frameworks), when inheriting from it and overriding methods
  • No repetitive delegation code clutters the implementation

Disadvantages

  • You can not inherit at runtime, binding to a super-class at runtime normally is not supported
  • Mostly you can not reuse several super-classes at the same time; some languages provide multiple inheritance (→ mixins), but you are going to be ambiguous, misleading and complex when you use that feature
  • Inheritance is a too intimate relationship, meaning the derived class can bring down the super-class by overrides or use of protected fields that are meant to be internal-only; that can be avoided only by accurate and well written access modifiers and field wrappers in the super-class, see chapter below

Delegation

Advantages

  • You can lazily load the delegate
  • You can bind the super-class delegate at runtime
  • You can simulate multiple inheritance

Disadvantages

  • Delegation is code duplication
  • Mostly the delegate needs an interface, so that all delegators can also implement it and forward to the delegate, which gives an additional interface and lots of purely technical method-forwarding in all delegators
  • When the delegate interface changes, you need to change both the delegate and all delegators
  • Without a delegate interface you can be sure that you will forget to change all delegators when the delegate changes!

How to Avoid Inheritance Break Encapsulation

Inheritance is said to be a too intimate relationship. E.g. you could modify non-final protected fields of a super-class, causing unexpected behavior. You could override a method and forget to call super, or do it in a way the super-class did not expect.

To avoid that, do the following in any super-class:

  1. Try to make all fields private, and as many methods as possible private, package-visible, or at least protected (minimize access modifiers)

  2. Make immutable protected fields final

  3. Make mutable protected fields private and a add a protected final getter for it (when necessary), try to avoid the setter

  4. When a private field has protected getter and setter, the field must not be accessed directly (in its class), only by its getter and setter, as they could be overwritten when being non-final

  5. Make all non-private methods called from constructor final, see here

  6. Add assertions to all non-private methods to uncover illegal object states

  7. Make all non-private methods final that are not explicitly meant to be overwritten, or that may endanger the object's state when overwritten

Should be self-evident, but belongs here: public fields always must be final (immutable)!

Conclusion

We need both of them, inheritance AND delegation!

Obviously delegation is the more flexible concept. But with more freedom comes less security, meaning the code is hard to maintain when having those big delegation sections in it.

Absence of abstraction means low quality of both specification and software. Code without abstractions will contain many duplications due to missing code reusage.

Respect business logic and its concepts. When the specification comes with an "IS A" relation, then use inheritance. When it comes with a "HAS A" relation, use delegation. When it comes with neither, tell them to introduce it :-)




Samstag, 19. Dezember 2020

Java Parallel versus Sequential Stream Performance

In my article about for-loop versus Java 8 stream loop I presented a test showing that a for-loop could be faster than a stream when doing typical sequential work like searching the maximum in a collection of numbers. This was due to the fact that parallel threads may be fast in processing their partition of the collection, but in the end they have to figure out who found the maximum, and this synchronization made the for-loop win the race.

What about another kind of work being done inside the loop? This Blog is about performing a long lasting task for each element of a list, and how this performs when doing it by a for loop, a stream forEach(), or a parallel stream forEach(). Mind that such work must be free of side-effects when done in parallel.

Test Source

The example builds a list of high positive numbers, then loops it and calls a method that counts from zero up to the given number from the list. That method is called longLastingTask(). The loop is performed using three different techniques:

  1. for (long limit : list)
        longLastingTask(limit)
  2. list.stream().forEach(limit -> longLastingTask(limit))
  3. list.parallelStream().forEach(limit -> longLastingTask(limit))

Here is the source code of the performance test, explained step by step. On bottom you find the entire class. There are no external libraries included, just the plain Java runtime environment.

public class LongTaskInLoopPerformance
{
    public static void main(String[] args) {
        System.out.println("Java: "+System.getProperty("java.version"));
        System.out.println("Cores: "+Runtime.getRuntime().availableProcessors());
        
        LongTaskInLoopPerformance streamPerformance = new LongTaskInLoopPerformance(30000);
        streamPerformance.performTests(10);
    }
    
    
    private final Collection<Long> longList;
    
    public LongTaskInLoopPerformance(final int numberOfCountLimits) {
        longList = createTestList(numberOfCountLimits);
    }
    
    private Collection<Long> createTestList(int testDataLength) {
        final List<Long> longList = new ArrayList<Long>(testDataLength);
        for (int i = 0; i < testDataLength; i++)    {
            long random = (long) Math.abs(Math.random() * 100000d);
            if (random > 0)
                longList.add(Long.valueOf(random));  // positive big numbers
        }
        return longList;
    }
    
    // more source goes here ...

}

This is the class skeleton with main method and constructor. The main method outputs the Java version and the number of cores in the machine where the test runs. Then it constructs a test with the length of the count-limit list set to 30000. Finally it runs 10 tests, so that warm-up and other influences are minimized.

In constructor, LongTaskInLoopPerformance builds the list of long numbers containing the limits for the long-lasting count tasks. Such a task will count from zero to the limit it receives as parameter. The list of limits stays unchanged during all test runs.

Next is the sum method. Put this to "// more source goes here ..." in class above:

    public void performTests(int numberOfTests) {
        long sumForLoop = 0L, sumParallelStream = 0L, sumSequentialStream = 0L;
        int numberOfForLoops = 0, numberOfParallelStreams = 0, numberOfSequentialStreams = 0; 
        
        final int numberOfTestTypes = 3;    // for-loop, sequential stream, parallel stream
        numberOfTests *= numberOfTestTypes;
        
        for (int testNumber = 0; testNumber < numberOfTests; testNumber++)   {
            final boolean doForLoop = (testNumber % numberOfTestTypes == 1);    // every 2nd test is for-loop
            final boolean useParallelStream = (testNumber % numberOfTestTypes == 2);    // every 3rd test is parallel
            
            long millis = performTest(doForLoop, useParallelStream);
            
            if (doForLoop)  {
                sumForLoop += millis;
                numberOfForLoops++;
            }
            else if (useParallelStream) {
                sumParallelStream += millis;
                numberOfParallelStreams++;
            }
            else    {
                sumSequentialStream += millis;
                numberOfSequentialStreams++;
            }
        }
        
        System.out.println(numberOfParallelStreams+" parallel stream forEach-loops needed "+sumParallelStream+" millis");
        System.out.println(numberOfForLoops+" for-loops needed "+sumForLoop+" millis");
        System.out.println(numberOfSequentialStreams+" sequential stream forEach-loops needed "+sumSequentialStream+" millis");
    }

This method sets up counters for every type of loop and their time sums. For every test to execute, all three types of loop are run. Every 1st test will be a sequential stream, every 2nd a for-loop, every 3rd a parallel stream. Finally it outputs the time sums for all three loop types.

The next method runs exactly one test:

    private long performTest(final boolean doForLoop, final boolean useParallelStream)    {
        final long before = System.currentTimeMillis();
        
        if (doForLoop)
            for (Long l : longList)
                longLastingTask(l);
        else if (useParallelStream)
            performStreamTest(longList.parallelStream());
        else
            performStreamTest(longList.stream());
        
        return (System.currentTimeMillis() - before);
    }

    private void performStreamTest(Stream<Long> streamOfLong) {
        streamOfLong.forEach(l -> longLastingTask(l));
    }

    private void longLastingTask(long limit)    {
        for (long i = 0; i < limit; i++)
            ;
    }

Here the test loop is executed in three different ways, determined by the parameters to performTest().

The performStreamTest() method contains the forEach(). Both .stream() and .parallelStream() methods produce the same data type Stream<Long>, although its processing differs.

The longLastingTask() method finally counts from zero to the limit it receives, which is a random number between 1 and 100000.


Click here to see the entire test-class for copy & paste.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
import java.util.*;
import java.util.stream.Stream;

/**
 * Check whether parallel streams outperform sequential ones and for-loops
 * when performing a long-lasting task with each element of a list.
 */
public class LongTaskInLoopPerformance
{
    public static void main(String[] args) {
        System.out.println("Java: "+System.getProperty("java.version"));
        System.out.println("Cores: "+Runtime.getRuntime().availableProcessors());
        
        LongTaskInLoopPerformance streamPerformance = new LongTaskInLoopPerformance(30000);
        streamPerformance.performTests(10);
    }
    
    
    private final Collection<Long> longList;
    
    public LongTaskInLoopPerformance(final int numberOfCountLimits) {
        longList = createTestList(numberOfCountLimits);
    }
    
    private Collection<Long> createTestList(int testDataLength) {
        final List<Long> longList = new ArrayList<Long>(testDataLength);
        for (int i = 0; i < testDataLength; i++)    {
            long random = (long) Math.abs(Math.random() * 100000d);
            if (random > 0)
                longList.add(Long.valueOf(random));  // positive big numbers
        }
        return longList;
    }
    
    public void performTests(int numberOfTests) {
        long sumForLoop = 0L, sumParallelStream = 0L, sumSequentialStream = 0L;
        int numberOfForLoops = 0, numberOfParallelStreams = 0, numberOfSequentialStreams = 0; 
        
        final int numberOfTestTypes = 3;    // for-loop, sequential stream, parallel stream
        numberOfTests *= numberOfTestTypes;
        
        for (int testNumber = 0; testNumber < numberOfTests; testNumber++)   {
            final boolean doForLoop = (testNumber % numberOfTestTypes == 1);    // every 2nd test is for-loop
            final boolean useParallelStream = (testNumber % numberOfTestTypes == 2);    // every 3rd test is parallel
            
            long millis = performTest(doForLoop, useParallelStream);
            
            if (doForLoop)  {
                sumForLoop += millis;
                numberOfForLoops++;
            }
            else if (useParallelStream) {
                sumParallelStream += millis;
                numberOfParallelStreams++;
            }
            else    {
                sumSequentialStream += millis;
                numberOfSequentialStreams++;
            }
        }
        
        System.out.println(numberOfParallelStreams+" parallel stream forEach-loops needed "+sumParallelStream+" millis");
        System.out.println(numberOfForLoops+" for-loops needed "+sumForLoop+" millis");
        System.out.println(numberOfSequentialStreams+" sequential stream forEach-loops needed "+sumSequentialStream+" millis");
    }
    
    private long performTest(final boolean doForLoop, final boolean useParallelStream)    {
        final long before = System.currentTimeMillis();
        
        if (doForLoop)
            for (Long l : longList)
                longLastingTask(l);
        else if (useParallelStream)
            performStreamTest(longList.parallelStream());
        else
            performStreamTest(longList.stream());
        
        return (System.currentTimeMillis() - before);
    }

    private void performStreamTest(Stream<Long> streamOfLong) {
        streamOfLong.forEach(l -> longLastingTask(l));
    }

    private void longLastingTask(long limit)    {
        for (long i = 0; i < limit; i++)
            ;
    }

}

Results

Here is the output for Java 8:

Java: 1.8.0_121
Cores: 4
10 parallel stream forEach-loops needed 3510 millis
10 for-loops needed 6379 millis
10 sequential stream forEach-loops needed 6724 millis

And here for Java 11:

Java: 11.0.2
Cores: 4
10 parallel stream forEach-loops needed 3774 millis
10 for-loops needed 6312 millis
10 sequential stream forEach-loops needed 6393 millis

Conclusion

For long lasting tasks without side effects, done for each element of a list, turning the list into a parallel stream absolutely makes sense. The parallel stream finished in half the time of the others, the for-loop being a little faster than the sequential stream.




Samstag, 28. November 2020

Ubuntu 20.04 Upgrade when Packages Have Been Kept Back

Ubuntu releases a new long-term-supported (LTS) distribution all two years. The 20.04 version (released April 2020) will be maintained until 2025. In LINUX, "update" means short-term- and "upgrade" means long-term-actuation. Today on this foggy, cold and lazy Saturday I dared to do an upgrade of my Ubuntu 18.04 laptop.

As I always save my data after I've worked on them, I did not do a backup (which is recommended). The "Software Updater" (update-manager) showed up some time after logging in, I clicked it to update. Then I started it again and pressed the "Upgrade" button for installing the new distribution.

I waited. Nothing happened. One minute, two minutes, ... they need lots sometimes ... launching the system monitor showed that nothing is going on. No error message, no info message, no update, no upgrade, nothing.
Yes, it is an open-source operating system:-)

Seaching for the Upgrade Problem

I began to read on the internet and experiment on command line.

$ sudo do-release-upgrade -d -f DistUpgradeViewGtk3 --allow-third-party

A graphical user interface came up telling me that I must update all packages first. Haven't I done this just before? So, once again:

$ sudo apt update
....
$ sudo apt upgrade
....
The following packages have been kept back:
  libsane-common libsane1
....

I overlooked the message about packages that have been kept back. I did a cleanup and then tried again the distro-upgrade:

$ sudo apt autoremove
....
$ sudo do-release-upgrade -d -f DistUpgradeViewGtk3 --allow-third-party

Again the graphical UI telling me that I must update all packages first. I tried out another upgrade-command:

$ sudo apt-get dist-upgrade
....
The following packages have been kept back:
  libsane-common libsane1
0 upgraded, 0 newly installed, 0 to remove and 2 not upgraded.

Even after this the upgrade did not work, telling me again that I have to update all packages first.

Now I assumed that the kept-back packages are the problem. I remembered that I once fixed, compiled and installed the "xsane" scanner software and protected it against system upgrades to not lose the fix. Now I had to update these libraries explicitly, possibly losing my fix (fortunately I wrote a Blog about it, so I can repeat it). Here is the force-update command:

$ sudo apt-get install libsane-common libsane1
....

After this, the upgrade through update-manager worked.

For about 90 minutes it installed packages on my 4 cores with 8 GB RAM. Many times the grub bootloader was updated, many times /boot/initrd.img-5.4.0-54 was generated. When shutting down it showed the Ubuntu logo together with the laptop's vendor logo (in my case "DELL"). I was thinking "Will I ever see you again?" ...

Ubuntu 20.04

On computer reboot I saw no Ubuntu/Linux version in my BIOS boot menu. You still have to edit /boot/grub/grub.cfg when you want such.
If you want console output instead of the splash screen during startup, also replace the line

    linux /boot/vmlinuz-5.4.0-54-generic root=UUID=your-linux-partition-uuid ro quiet splash $vt_handoff

by

    linux /boot/vmlinuz-5.4.0-54-generic root=UUID=your-linux-partition-uuid ro

in /boot/grub/grub.cfg.

Startup has not become faster. Was fast once, but currently is no comparison to WINDOWS 10.

The desktop environment came up with a black mouse cursor (was white in 18.04), black top bar, and a grey semi-transparent side bar. Graphics seem to have improved, very sharp contours.

My auto-login was removed, I had to re-enable it in Settings under "Users".

Desktop screenshot:

Like on 18.04 upgrade all desktop icons were disabled, but with right mouse context menu I could "Allow" them. Launching a graphical application then from one of these icons showed white text color on light-gray background in title-bar, not readable, but some minutes later the title-bar background magically turned to black.

Desktop side bars are now possible on more than one screen. With "Auto-hide the Dock" (Settings -> Appearance) activated they are visible anyway, they hide only when some window overlaps them. When having configured a second side-bar on right monitor, and a full-screen app is on right monitor too, moving the mouse to the left monitor is kind of impossible. The side-bar pops out any time the mouse is over it , and the mouse hangs there. I switched off this feature immediately.

The Alt-Tab key (switch between application windows) did not work any more. It was changed to Super-Tab, I had to restore that in Settings - "Keyboard Shortcuts".

The Constant LINUX Screenshot Trouble

A good screenshot utility is an indispensable work tool. You need it for quickly and precisely communicating bugs on user-interfaces. Adding annotations on such screenshots is a must, point to the fail with an arrow, or frame it with an ellipse. The most useful graphical element is a speech bubble that tells the problem and points to it.

Linux screenshot tools were never sufficient. I found out that Shutter (my previously installed screenshot tool) had disappeared silently from my side bar through the upgrade, so I had to search for a replacement.

Flameshot

A very unusual UI, super-modern. You can add annotations, but you can not modify, move, size or remove it afterwards. There are just "Undo" and "Redo" actions. There are lines, arrows, circles, full or empty rectangles, and text, but no speech bubbles. You must find out that the SPACE key slides in a left-side panel, providing a generic "Thickness" chooser where you can size the currently written text or figure. Edit buttons are arranged around the screenshot image in different layouts, depending on the dimension of the shot, which is a little bit confusing, because you need to find the "Save" button on every screenshot newly among many many others.

Following sceenshot shows the launcher in top-bar:

This comes up when you click "Open Launcher":

Some short help:

Ksnip

Hard to remember name. Ksnip has a better editor than Flameshot. It lets modify, move, size or remove markup after it has been drawn (if you find the "Select" button). The concept is the same as in "Greenshot" (a free WINDOWS tool), but Ksnip also has no speech bubbles.

Conclusion

Happy that my upgrade worked. Upgrades are indispensable. If you stay back too long, you will be lost. We depend on the Internet, the Internet depends on web-browsers. Browsers are evolving fast and change all the time. As they open more and more hardware possibilities, they depend on a current operating systems.

I am really grateful for this free operating system. I've been using it for 23 years now, and always preferred it to WINDOWS. But yes, I am a developer.




Samstag, 21. November 2020

Java Module Types

The Java Platform Module System (JPMS) defines different types of modules. This is to ease the migration from old CLASSPATH bound projects to the new module-path way. In an ideal world, just real JPMS modules exist, no other types. But, as some open-source libraries are not maintained any more and may never become modules, it is questionable whether that world will ever exist.

Type Designations

Module Type Description Can Access No Access To
Java Modules
(Named Modules,
Explicit Modules)
One or more Java packages that have a module-info.java descriptor in their root directory (dependency definitions).
Sub-types are ...
System Modules Java runtime libraries like java.base that need no requires statement to be available
Application Modules whatever your application defines as module
Service Modules a module that uses the provides keyword
Open Modules a module that uses the open keyword to allow reflection on its classes
System modules and all modules that it requires (in case it was not prohibited via exports somepackage to somemodule), also Automatic modules that it requires The Unnamed Module
Automatic Modules A Java archive (JAR) file without module-info.java that has been put on the module-path. All its packages will be exported automatically and are open for reflection. It has an automatically generated name, built from the basename of the JAR file, by which it can be required in a named module.
Legacy JARs go here.
All exported packages of modules on the module-path, also the Unnamed Module. Module packages that were not exported
The Unnamed Module Everything that is on CLASSPATH. All packages get exported automatically and are open for reflection. This module has no name, thus it can not be required by modules. All exported packages of modules on the module-path, also Automatic modules, and all packages on CLASSPATH. Module packages that were not exported

How to Use

The JDK runtime library has been modularized to System Modules. Any application that wants to upgrade to Java 9 can use them by going to the Unnamed Module, together with all its JAR dependencies (CLASSPATH). For that it is not even necessary to define a module-path.

The only drawback is the "Split Packages" constraint. Java compilers above version 9 don't accept two packages with the same name on CLASSPATH. Typical error message is

  The package javax.xml.transform is accessible from more than one module: <unnamed>, java.xml

when you have e.g. xml-apis.jar on the CLASSPATH as transitive Maven dependency. Normally this can be solved with Maven <exclusions>. The "Split Packages" constraint detects different versions of the same library, a big problem of CLASSPATH-bound applications.

As soon as you modularize your application, all JAR dependencies must go to the module-path as Automatic Modules and have to be required in module-info.java, because Named Modules can not access the Unnamed Module (CLASSPATH) any more. Running the application then will not work without --module-path option. Mind that the module-info.java descriptor can be extended through JVM commandline options like --add-modules, --add-exports, --add-reads.

The "Split Packages" constraint would not work when both CLASSPATH and module-path are active. If there were two packages with same name, one on CLASSPATH and one on module-path, the one on module-path would be silently preferred.

Conclusion

The different module types make it easy to migrate applications to a Java version above 9. CLASSPATH is represented by the Unnamed Module, Named Modules are intended to be on module-path. Like the CLASSPATH, the module-path plays a role at both compile-time and runtime. In future it will replace the CLASSPATH.

JPMS modules are made to be checked at compile-time. They can not be replaced at runtime like OSGI plugins can, they can not be started or stopped, and they do not support versioning. Their only runtime-bound behavior is the service providing via provides and uses.