Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-8335984

Simple Source Files and Instance Main Methods (Fourth Preview)

XMLWordPrintable

    • Icon: JEP JEP
    • Resolution: Unresolved
    • Icon: P4 P4
    • None
    • specification
    • None
    • Ron Pressler, Jim Laskey, & Gavin Bierman
    • Feature
    • Open
    • JDK
    • amber dash dev at openjdk dot org

      Summary

      Evolve the Java programming language so that beginners can write their first programs without needing to understand language features designed for large programs. Far from using a separate dialect of the language, beginners can write streamlined declarations for single-class programs and then seamlessly expand their programs to use more advanced features as their skills grow. Experienced developers can likewise enjoy writing small programs succinctly, without the need for constructs intended for programming in the large. This is a

      preview<br /> language feature

      .

      History

      This feature was first proposed for preview by JEP 445 (JDK 21) and subsequently improved and refined by JEP 463 (JDK 22) and JEP 477 (JDK 23). We here propose to preview it for a fourth time, with a revised title but otherwise unchanged, in order to gain additional experience and feedback.

      Goals

      • Offer a smooth on-ramp to Java programming, so that instructors can introduce concepts in a gradual manner.

      • Help students write basic programs in a concise manner, and grow their code gracefully as their skills grow.

      • Reduce the ceremony of writing other kinds of small programs, such as scripts and command-line utilities.

      • Do not introduce a separate dialect of the Java language.

      • Do not introduce a separate toolchain; small Java programs should be compiled and run with the same tools as large programs.

      Motivation

      The Java programming language excels for large, complex applications developed and maintained over many years by large teams. It has rich features for data hiding, reuse, access control, namespace management, and modularity which allow components to be cleanly composed while being developed and maintained independently. With these features, components can expose well-defined interfaces for their interaction with other components while hiding internal implementation details so as to permit the independent evolution of each. Indeed, the object-oriented paradigm itself is designed for plugging together pieces that interact through well-defined protocols and abstract away implementation details. This composition of large components is called programming in the large.

      The Java programming language is also, however, intended to be a first language. When programmers first start out they do not write large programs, in a team — they write small programs, alone. They have no need for encapsulation and namespaces, useful to separately evolve components written by different people. When teaching programming, instructors start with the basic programming in the small concepts of variables, control flow, and subroutines. At that stage there is no need for the programming-in-the-large concepts of classes, packages, and modules. Making the language more welcoming to newcomers is in the interest of Java veterans but they, too, may enjoy writing small programs more concisely, without any programming-in-the-large constructs.

      Consider the classic

      [Hello,
      World!
      |https://en.wikipedia.org/wiki/%22Hello,_World!%22_program]

      example that is often a beginner's first program:

      public class HelloWorld {
          public static void main(String[] args) {
              System.out.println("Hello, World!");
          }
      }

      There is too much clutter here — too much code, too many concepts, and too many constructs — for what the program does.

      • The class declaration and the mandatory public access modifier are programming-in-the-large constructs. They are useful when encapsulating a code unit with a well-defined interface to external components, but pointless in this little example.

      • The String[] args parameter also exists to interface the code with an external component, in this case the operating system's shell. It is mysterious and unhelpful here, especially since it is not used in small programs such as HelloWorld.

      • The static modifier is part of the language's class-and-object model. For the beginner, static is not just mysterious, but harmful: To add more methods or fields to this program, the beginner must either declare them all as static — thereby propagating an idiom which is neither common nor a good habit — or else confront the difference between static and instance members and learn how to instantiate an object.

      • The beginner may be further puzzled by the mysterious incantation System.out.println, and wonder why a simple function call does not suffice. Even in first-week programs, the beginner may be forced to learn how to import basic utility classes for essential functionality, and wonder why they could not be provided automatically.

      The new programmer encounters these concepts at the worst possible time, before they learn about variables and control flow, and when they cannot appreciate the utility of programming-in-the-large constructs for keeping a large program well organized. Instructors often offer the admonition, "don't worry about that, you'll understand it later." This is unsatisfying to they and their students alike, and leaves students with the enduring impression that the language is complicated.

      The motivation for this work is not merely to reduce ceremony. We aim to help programmers that are new to the Java language, or to programming in general, learn the language in a manner that introduces concepts in the right order: Start with the fundamental programming-in-the-small concepts, such as doing simple textual I/O and processing arrays with for loops, and proceed to advanced programming-in-the-large concepts only when they are actually beneficial and can be more easily grasped.

      The motivation for this work is, moreover, not only to help beginning programmers. We aim to help everyone who writes small programs, whether they be students, system administrators writing command-line utilities, or domain experts prototyping core algorithms that will eventually be used in the heart of an enterprise-scale software system.

      We propose to make it easier to write small programs not by changing the structure of the Java language — code is still enclosed in methods, which are enclosed in classes, which are enclosed in packages, which are enclosed in modules — but by hiding such details until they are useful. We offer an on-ramp, that is, a gradual incline that merges gracefully onto the highway. As beginners move on to larger programs, they need not discard what they learned in the early stages, but, rather, they see how it all fits within the larger picture. As experienced developers proceed from prototype to production, they can smoothly grow their code into components of larger programs.

      Description

      First, we enhance the protocol by which Java programs are launched to allow instance main methods. Such methods are not static, need not be public, and need not have a String[] parameter. Then we can simplify the Hello, World! program to:

      class HelloWorld {
          void main() {
              System.out.println("Hello, World!");
          }
      }

      Second, we allow a simple source file to implicitly declare a class:

      void main() {
          System.out.println("Hello, World!");
      }

      Third, in simple source files we automatically import useful methods for textual input and output, thereby avoiding the mysterious System.out.println:

      void main() {
          println("Hello, World!");
      }

      Finally, for programs that go beyond Hello, World! and need, for example, basic data structures or file I/O, in simple source files we automatically import, on demand, all of the public top-level classes and interfaces of all of the packages exported by the java.base module.

      This is a preview language feature, disabled by default

      To try the examples below in JDK 24 you must enable preview features:

      The launch protocol

      New programmers just want to write and run a computer program. The

      <em>Java<br /> Language<br /> Specification</em>

      (JLS), however, focuses on defining the meaning of a

      <em>compilation<br /> unit</em>

      , that is, a source file with a package declaration, import declarations, and class declarations. All that the JLS has to say about a Java program is this:

      The Java Virtual Machine starts execution by invoking the method main of some specified class or interface, passing it a single argument which is an array of strings.

      The JLS further says:

      The manner in which the initial class or interface is specified to the Java Virtual Machine is beyond the scope of this specification, but it is typical, in host environments that use command lines, for the fully qualified name of the class or interface to be specified as a command line argument and for following command line arguments to be used as strings to be provided as the argument to the method main.

      The actions of choosing the class containing the main method, assembling its dependencies in the form of a module path or a class path (or both), loading the class, initializing it, and invoking the main method with its arguments constitute the launch protocol. In the JDK it is implemented by the launcher, i.e., the java executable.

      A flexible launch protocol

      We enhance the launch protocol to offer more flexibility in the declaration of a program's entry point and, in particular, to allow instance main methods, as follows:

      • Allow the main method of a launched class to have public, protected, or default (i.e., package) access.

      • If the launched class contains a main method with a String[] parameter then choose that method.

      • Otherwise, if the class contains a main method with no parameters then choose that method.

      • In either case, if the chosen method is static then simply invoke it.

      • Otherwise, the chosen method is an instance method and the launched class must have a zero-parameter, non-private constructor (i.e., of public, protected, or package access). Invoke that constructor and then invoke the main method of the resulting object. If there is no such constructor then report an error and terminate.

      • If there is no suitable main method then report an error and terminate.

      These changes allow us to write Hello, World! with no access modifiers, no static modifiers, and no String[] parameter, so the introduction of those constructs can be postponed until they are needed:

      class HelloWorld {
          void main() {
              System.out.println("Hello, World!");
          }
      }

      Implicitly declared classes in simple source files

      In the Java language, every class resides in a package and every package resides in a module. These namespacing and encapsulation constructs apply to all code, but small programs that do not need them can omit them. A program that does not need class namespaces can omit the package statement, making its classes implicit members of the unnamed package; classes in the unnamed package cannot be referenced explicitly by classes in named packages. A program that does not need to encapsulate its packages can omit the module declaration, making its packages implicit members of the unnamed module; packages in the unnamed module cannot be referenced explicitly by packages in named modules.

      Before classes serve their main purpose as templates for the construction of objects, they serve only as namespaces for methods and fields. We should not require beginners to confront the concept of classes before they are comfortable with the more basic building blocks of variables, control flow, and subroutines, before they embark on learning object orientation, and when they are still writing simple, single-file programs. Even though every method resides in a class, we can stop requiring explicit class declarations for code that does not need it — just as we do not require explicit package or module declarations for code that does not need them.

      Henceforth, if the Java compiler encounters a source file with a method that is not enclosed in a class declaration then it will consider the source file to implicitly declare a class whose members consist of that method along with any other unenclosed methods, fields, and classes in the file. Such a source file is called a simple source file.

      The implicitly declared class of a simple source file is always a final, top-level class. It extends Object and does not implement any interfaces. It cannot be referenced by name, thus it cannot be instantiated directly and there cannot be any method references to any static methods. The this keyword can still be used, however, as can method references to instance methods.

      The implicitly declared class of a simple source file is useful only as a standalone program or as an entry point to a program. Thus a simple source file must have a main method that can be launched as described above. This requirement is enforced by the Java compiler.

      The implicitly declared class of a simple source file resides in the unnamed package, and the unnamed package resides in the unnamed module. While there can be only one unnamed package and only one unnamed module (barring multiple class loaders), there can be multiple implicitly declared classes in the unnamed module. Each such class contains a main method and so represents a program, thus multiple such classes represent multiple programs.

      The unenclosed methods, fields, and classes of a simple source file are the members of the file's implicitly declared class. They can have the same modifiers (e.g., private and static) as the members of any other class, and the modifiers have the same defaults (e.g., package access and instance membership). The implicitly declared class of a simple source file has a default zero-parameter constructor; it does not have any other constructors.

      With these changes we can now write Hello, World! as:

      void main() {
          System.out.println("Hello, World!");
      }

      Since top-level members of a simple source file are interpreted as members of the file's implicitly declared class, we can also write the program as:

      String greeting() { return "Hello, World!"; }
      
      void main() {
          System.out.println(greeting());
      }

      or, using a field, as:

      String greeting = "Hello, World!";
      
      void main() {
          System.out.println(greeting);
      }

      If the simple source file has an unenclosed instance main method rather than an unenclosed static main method then launching it is equivalent to embedding it in an anonymous class declaration and invoking that, i.e.:

      new Object() {
      
          String greeting = "Hello, World!";
      
          void main() {
              System.out.println(greeting);
          }
      
      }.main();

      A simple source file named HelloWorld.java can be launched with the source-code launcher, like so:

      $ java HelloWorld.java

      The Java compiler will compile the file to the class file HelloWorld.class, and then the launcher will launch that file. The compiler chooses HelloWorld for the class name as an implementation detail, but that name still cannot be used directly in source code.

      The javadoc tool can generate documentation for the implicitly declared class of a simple source file, even though the class cannot be referenced by other classes and thus cannot be used to define an API. Still, the ability to generate documentation for the members of the implicitly declared class may be useful, both for beginners learning to document their code, and for experienced developers prototyping code intended to be used in a larger program or writing reusable scripts for execution by the source code launcher.

      Interacting with the console

      Many beginner programs need to interact with the console. Writing to the console ought to be a simple method invocation, but in reality it requires using the qualified name System.out.println. This is mildly painful for experienced developers, but deeply mysterious to the beginner: What is System, what is out, and what are the dots for?

      Even worse is reading from the console which, again, ought to be a simple method invocation. Since writing to the console involves System.out, it seems reasonable to try reading from System.in. But reading a String from System.in requires all this code:

      try {
          BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
          String line = reader.readLine();
          ...
      } catch (IOException ioe) {
          ...
      }

      Experienced developers are used to this boilerplate, but for the beginner this code contains yet more mysterious concepts, leading to a plethora of questions: What are try and catch for, why both BufferedReader and InputStreamReader, and what is an IOException? There are other approaches, but none is significantly better, especially for the beginner.

      To simplify the writing of interactive small programs, we make three methods available for use in simple source files:

      public static void println(Object obj);
      public static void print(Object obj);
      public static String readln(String prompt);

      A beginner can now write Hello, World! as:

      void main() {
          println("Hello, World!");
      }

      They can then easily move on to the simplest of interactive programs:

      void main() {
          String name = readln("Please enter your name: ");
          print("Pleased to meet you, ");
          println(name);
      }

      We achieve this effect by declaring a new top-level class in the java.io package named, simply, IO. It declares the above three static methods for textual I/O with the console, and nothing else. Every implicitly declared class automatically imports these static methods, as if the declaration

      import static java.io.IO.*;

      appears at the start of every simple source file.

      The new class java.io.IO is a preview API in JDK 24.

      Automatic import of the java.base module

      Many other classes declared in the Java API are useful in small programs. They can, of course, be imported explicitly at the start of a simple source file:

      import java.util.List;
      
      void main() {
          var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
          for (var name : authors) {
              println(name + ": " + name.length());
          }
      }

      Experienced developers will find this natural, though for convenience some might be inclined to use import-on-demand declarations (i.e., import java.util.*). For beginners, however, any form of import is another source of mystery, requiring an understanding of the package hierarchy of the Java API.

      To further simplify the writing of small programs, we make all of the public top-level classes and interfaces of the packages exported by the java.base module available for use in simple source files, as if they were imported on demand. Popular APIs in commonly used packages such as java.io, java.math, and java.util are thus immediately usable, without any fuss. In the above example, the import java.util.List declaration can be removed since the interface will be imported automatically.

      A companion JEP proposes a new import declaration, import module M, which imports, on demand, all of the public top-level class and interfaces of the packages exported by module M. Thus every implicitly declared class can be considered to implicitly import the java.base module, as if the declaration

      import module java.base;

      appears at the start of every simple source file.

      Growing a program

      A small program in a simple source file is much more focused on what the program actually does, omitting concepts and constructs it does not need. Even so, all members are interpreted just as in an ordinary class. To evolve a simple source file into a ordinary source file, all we need to do is wrap its declaration, excluding any import declarations, inside an explicit class declaration, and add the automatic imports. For example, this simple source file:

      void main() {
          var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
          for (var name : authors) {
              println(name + ": " + name.length());
          }
      }

      can be grown into this ordinary source file containing a single top-level class declaration:

      import static java.io.IO.*;
      import module java.base;
      
      class NameLengths {
      
          void main() {
              var authors = List.of("James", "Bill", "Guy", "Alex", "Dan", "Gavin");
              for (var name : authors) {
                  println(name + ": " + name.length());
              }
          }
      
      }

      The main method does not change in any way. Turning a small program into one that can serve as a component in a larger program is straightforward, particularly for beginning programmers.

      Eliminating the main method altogether may seem like the next natural step, but it would work against the goal of gracefully evolving small Java programs to larger ones and would impose some non-obvious restrictions (see below). Dropping the void method result would, similarly, create a distinct Java dialect.

      Alternatives

      • _Use blank" rel="nofollow noopener" data-shared-secret="1728795948596-0.7288852417036409">JShell for small programs — A JShell session is not a program, but a sequence of code snippets. Declarations typed into jshell are implicitly viewed as static members of some unspecified class, with some unspecified access level, and statements execute in a context in which all previous declarations are in scope.

        This is convenient for experimentation — which is the primary use case for JShell — but not a good model for writing small programs. Evolving a batch of working declarations in JShell into a real program leads to a non-idiomatic style of code because it declares each method, class, and variable as static. JShell is a great tool for exploration and debugging, but it is not the on-ramp programming model we are looking for.

      • Interpret code units as static members — Methods and fields are non-static by default. Interpreting top-level members in a simple source file as static would change the meaning of the code units in such a class — introducing, in effect, a distinct Java dialect. To preserve the meaning of such members when we evolve a simple source file into an ordinary source file we would have to add explicit static modifiers. When we scale up from a handful of methods to a simple class we want to use classes as classes, not merely containers of static members.

      • Interpret the source file as a method body — Rather than treat a simple source file as the body of an implicitly declared class, we could treat it as the body of a main method of an implicitly declared class. A simple source file could contain only statements, local class or interface declarations, or local variable declarations, without the header of the main method.

        This approach would be limiting since there would be no way to declare auxiliary methods; we could only write linear programs, without the power to abstract repeating computations into subroutines. Moreover, there would be no way to declare fields; all variable declarations would be treated as local variable declarations. This is a limitation because local variables can only be accessed from inside lambda bodies or inner classes when they are effectively final, whereas fields do not have that constraint.

        The design we propose above supports both auxiliary methods and field declarations. The burden of writing a main method header, especially with the relaxed requirements on this method in a simple source file, is not onerous, even for beginners.

      • Introduce package-level methods and fields — We could achieve a user experience similar to that shown above by allowing package-level methods and fields to be declared in a file without an explicit package or class declaration. However, such a feature would have a far wider impact on how Java code is written in general.

      • Different automatic imports — Rather than have the class implicitly declared by a simple source file import all 54 packages in the java.base module on demand, we could instead have it import some subset of those packages. But, which subset should that be?

        Every reader will have suggestions for which packages to auto-import into every small program: java.io and java.util would be near-universal suggestions; java.util.stream and java.util.function would be common; and java.math, java.net, and java.time would each have supporters. For the JShell tool, we managed to find ten java.* packages which are broadly useful when experimenting with one-off Java code, but it is difficult to see which subset of java.* packages deserves to be permanently and automatically imported into every small program. The list would, moreover, change as the Java Platform evolves; e.g., java.util.stream and java.util.function were introduced only in Java 8. Developers would likely become reliant on IDEs to remind them of which automatic imports are in effect — an undesirable outcome.

        Importing all of the packages exported by the java.base module is a consistent and reasonable choice for classes implicitly declared by simple source files.

            gbierman Gavin Bierman
            gbierman Gavin Bierman
            Gavin Bierman Gavin Bierman
            Brian Goetz
            Votes:
            0 Vote for this issue
            Watchers:
            5 Start watching this issue

              Created:
              Updated: