Java has an incredible open source community and there are amazing high-quality packages available to solve any problem under the sun; and so it is common for Java apps to have large dependency graphs. For example, take Google Guava, a popular and powerful Java library. It's so useful that other libraries depend on it. Let's say that our application uses two libraries called Component-A and Component-B, which both depend on Guava. However, Guava is constantly changing, and so it may arise that Component-A depends on Guava version 17 while Component-B depends on Guava version 20.
This is called a version conflict, and Java packages aren't enough to solve the problem well, though module systems can. For example, a module system allows you, as the application owner, to override the Guava version used by Component-A, replacing Guava-17 with Guava-20. If Component-A is compatible with Guava-20, then that resolves the problem. Another solution module systems provide is to allow both Guava-17 and Guava-20 to coexist in the same application, used separately by Component-A and Component-B. If those components are using Guava 'privately', as part of their implementation only, then this might also be an adequate resolution.
The default Java package system does not have a notion that there may be multiple implementations of a class like `com.google.common.base.Preconditions`, and that you may wish to select one version of it or another, or that you may want to use different versions in different parts of the application. It's not normally desirable to tamper with dependency versions or load multiple versions simultaneously, but version conflicts happen and module systems give you tools to deal with them when they occur. Fortunately, the behavior of Java packages is determined by Java ClassLoaders, and module systems can implement their own ClassLoaders to provide dynamic behaviors like these that go beyond vanilla Java's capabilities.
Since Java supports reflection, with regular Java packages, any class can access any other class and class member in the app, regardless of whether it's package private. These reflection capabilities are super useful; they're what enable frameworks like Spring, Guice, EasyMock, and Jackson. However, unrestricted reflective access is also a liability (viz. Ruby Monkey Patching), and it's something that you'd prefer to outright disable or at least sandbox in most components you depend on. But since these components might legitimately require reflection in their implementation, like using Guice for dependency injection or EasyMock for testing, you may not be able to disable reflection entirely; instead, module systems can help you sandbox reflection so that it operates within the confines of the module, to make the system simpler and more predictable.
Some module systems like OSGi also extend into the runtime behavior of your application, and help you manage complex applications. Certain very complex Java applications act a bit like an operating system: there are multiple services within the app that communicate with each other, and the module system isolates them for encapsulation reasons, loads them in the appropriate order, connects them together through exported interfaces, and provides management actions like reloading a module while the rest of the app keeps running. These module systems are for Java apps what systemd is for Linux (including the fact that some people love them while others think they're overly complex and should be avoided).
Some of these purported issues with Jigsaw appear to be quite complex and a bit difficult for the layman Java user to understand.
Jigsaw totally punts on the problem of incompatible versions. In fact, it just doesn't do dependency versioning; only module versioning.
I am going to sully it by weighing in with my opinion:
> Java packages are a fine "module" system if you're designing a single large cohesive application that has no dependencies or trust boundaries.
I've always thought that Java packages are a fine module system period. They require library authors to be prescient enough to never make backward-incompatible API changes (it's easy, just make a new package any time you want to change your API in an incompatible way).
Obviously, the open-source community has fallen down on the job in this respect, and I'm sure there is a widespread opinion that the burden of managing API changes in this way will never be met and so something like Jigsaw is necessary.
But I think the required culture change among library authors would be much preferable to adding the complexity of another module system on top of packages. And it's such an easy change for library authors to make to their behavior that there must be at least some hope of being able to reach a consensus around it.
Especially of interest is this introductory video: https://www.youtube.com/watch?v=2Hmrn_r-uJA