Practice 11
The material for the practice¶
Splitting strings¶
The split() method of Strings¶
We will look at two ways of splitting text, the first is that String
objects have a method called split()
which takes a regular expression as a parameter, splits the text along that, and returns a String array containing the text chunks. Read more about the String#split() method.
String sentence = "This sentence consists of six words.";
String[] words = sentence.split(" ");
for(int i=0; i < words.length; i++) {
System.out.println("The " + i + ". word: " + words[i]);
}
The output of this is:
The 0. word: This
The 1. word: sentence
The 2. word: consists
The 3. word: of
The 4. word: six
The 5. word: words.
StringTokenizer¶
Another option is to use a class called StringTokenizer
, which is included in the java.util
package, which we need to import first. We can then use the class in our code, which we need to instantiate, expecting a text in the constructor, and optionally a text with each character representing a word boundary. After that we can use our StringTokenizer
object, which has a method called hasMoreTokens()
, which tells us if there are any more elements in the fragmented text: it returns true if there are, and false otherwise. To retrieve a fragment of a word, you can use the nextToken()
method, which returns the current text fragment.
The default separators for StringTokenizer are the following characters: " \t\n\r\f"
- space
- tab character
- newline character
- carriage return character
- line feed
If you want to split your text with characters other than these, you can specify the characters you want to split along as a second parameter in the constructor (splits at any character in the string).
More about the StringTokenizer class
import java.util.StringTokenizer;
public class Main {
public static void main(String[] args) {
String str = "abcd, text,something kitten;puppy;bear snowman shovel";
// create StringTokenizer with default separator
StringTokenizer st = new StringTokenizer(str);
System.out.println("StringTokenizer first run on str: (with default separator)");
while (st.hasMoreTokens()) {
String tmp = st.nextToken();
System.out.println(tmp);
}
System.out.println();
System.out.println("StringTokenizer second run on str: (with ; , . separators)");
st = new StringTokenizer(str, ";.,");
while (st.hasMoreTokens()) {
String tmp = st.nextToken();
System.out.println(tmp);
}
}
}
Its output:
StringTokenizer first run on str: (with default separator)
abcd,
text,something
kitten;puppy;bear
snowman
shovel
StringTokenizer second run on str: (with ; , . separators)
abcd
text
something kitten
puppy
bear snowman shovel
Read from standard input¶
As we saw in Practice 1, to scan, we create a new Scanner
object and pass it the in
data member of the System
class. The Scanner
can scan from a variety of inputs (including files), so its constructor expects an InputStream
object. This will be System.in
in our case.
import java.util.Scanner;
public class Reading {
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
System.out.println("Hello! What's your name?");
String name = sc.nextLine();
System.out.println("Hello " + name + "! How old are you?");
int age = sc.nextInt();
System.out.println("Hello " + name + ", who is " + age + " years old.");
}
}
If you want to use the Scanner
that reads from standard input in multiple methods within a class, then you should store it in a static
data member. It is unnecessary to create a new instance of it each time you use it.
import java.util.Scanner;
public class Main {
private static Scanner scanner = new Scanner(System.in);
public static void main(String[] args) {
scanner.nextLine();
method1();
}
private static void method1() {
scanner.nextLine();
}
}
File management¶
In Java, file handling is also done through objects, but since files are resources shared between programs, they must be handled a little differently than "traditional" objects. When a file management object is created, it opens the file for writing or reading via the operating system. As long as your program keeps a file open, access to it from another program may be restricted. For example, if a file is opened for reading, no other program can delete or even modify that file. Therefore, it is important to "close" a file as soon as it is no longer needed, keeping it "occupied" as little as possible. Of course, when the Garbage Collector frees our file management object, it will automatically close the file, but we don't know in advance when this will happen, it can even be much later than the last time we actually used the file. If we have written to the file, its contents may remain in memory (buffer) and not even be written to disk until that moment.
There are also various errors that can occur during file management, which we also need to pay attention to. This can be the case, for example, if there is no file to open for reading, we don't have the right to write to a file, the disk is full, etc. In these cases, of course, exceptions are thrown by the methods of the file handler objects, which we have to handle.
The file handling classes are in the java.io
package because they implement I/O, i.e. input/output operations.
The old way¶
Read file¶
In the following example, we use the familiar java.util.Scanner
class to read a complete file line by line and write its contents to the default output.
import java.io.*;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
Scanner scanner = null;
try {
scanner = new Scanner(new File(args[0]));
while(scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
} catch (IOException e) {
System.err.println("An error occurred: " + e.getMessage());
} finally {
if (scanner != null) {
scanner.close();
}
}
}
}
It may seem daunting, perhaps it is a little, but this example will give you an idea of the many places where errors can occur when working with files. Practically anywhere. Let's go through the code line by line.
First, we create a reference of type Scanner
, but we give it an initial value of null
, because we have to embed the instantiation in a try
block, since the constructor itself can throw an exception (for example, if the file does not exist), and later we want to use the reference in the finally
block. In the example, we used the first command line parameter, which should be the path to a file, from which we create a java.io.File
object, which we pass directly to the Scanner
constructor. If the file is successfully opened (no exception is thrown by the constructor), we proceed. In the while
loop condition, we use the Scanner.hasNextLine()
method, which returns a boolean
value (true
if there is more of the file, false
if we have reached the end, as appropriate), and then in the cycle core, we use the System.out.println()
method to print the line scanned by nextLine()
to the output.
Note: The
File
class represents a file system object in terms of its path. It can provide information about it (e.g., size, modification time, permissions, etc.) and perform certain operations on it (modify its properties, rename it, create an empty file, create a folder, delete it, etc.).
The catch
block handles any errors that may occur during copying or reading, which will be descendants of the java.io.IOException
class (e.g. FileNotFoundException
, AccessDeniedException
). You might think you are done, but (looking at the code) you are far from it. Because if we get an error during a read operation (for example, we are in the middle of a file when we suddenly lose the right to read the file), the close()
method would not be called. For such cases, we also write a finally
block to try
in which, if we have managed to instantiate the Scanner
object at all, we close it, freeing up the reserved resource as soon as we don't need it.
Write file¶
Writing a file is done very similarly to reading. We'll use the java.io.PrintStream
class, which may already be familiar since the System.out
data member is of the same type. In the example, we will print the other arguments to the file we receive as the first argument.
import java.io.*;
public class Main {
public static void main(String[] args) {
PrintStream printStream = null;
try {
printStream = new PrintStream(args[0]);
for (int i = 1; i < args.length; ++i) {
printStream.println(args[i]);
}
} catch (IOException e) {
System.err.println("An error occurred: " + e.getMessage());
} finally {
if (printStream != null) {
printStream.close();
}
}
}
}
The structure is virtually identical to the one seen at reading.
Try-with-resources¶
Since Java 7, the above two examples can be written more concisely using the try-with-resources
structure. This is a "tuned" try
block that handles one or more resources.
import java.io.*;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
try (Scanner scanner = new Scanner(new File(args[0]))) {
while(scanner.hasNextLine()) {
System.out.println(scanner.nextLine());
}
} catch (IOException e) {
System.err.println("An error occurred: " + e.getMessage());
}
}
}
The point is that the variable(s) declared in parentheses will only be accessible in its scope (as opposed to the code above, where the method is in scope), and will be automatically closed as soon as we leave the block (in the background, it generates a finally
block at the end of try
, like the one we wrote above). For it to work, the resource must implement the AutoCloseable
interface (which expects a single close()
method), as all built-in IO classes do.
Let's see an example of try-with-resources
handling multiple resources. The following code copies the contents of one file to another one line by line.
import java.io.*;
import java.util.Scanner;
public class Main {
public static void main(String[] args) {
try (
Scanner scanner = new Scanner(new File(args[0]));
PrintStream printStream = new PrintStream(args[1])
) {
while(scanner.hasNextLine()) {
printStream.println(scanner.nextLine());
}
} catch (IOException e) {
System.err.println("An error occurred: " + e.getMessage());
}
}
}
As you can see, you can put any number of resource declarations separated by semicolons in the try
brackets. If an exception is thrown when any of them are opened, the ones already opened are automatically closed, which was particularly cumbersome and ugly with the old method. The while
loop we've seen before works in the block, this time printing out using the printStream
object we opened instead of System.out
.
Lambda expressions¶
In the case of an application with a graphical interface, when we write an event handler for a button (there's an example of this in 08-Programozas-I.pdf
, and another code here), we create an anonymous interface implementation, which makes the code less transparent and much harder to read. In addition, we also get a lot of redundant code fragments in our code, which we can easily avoid from Java 8 by using lambda expressions.
Lambda functions are basically anonymous methods that you write where you want to use them. In practice, they can be useful if, for example, you want to implement an interface locally that has only one method, or for the efficient, fast, and transparent traversal of collections. So you can write an interface implementation faster, more concisely, and more transparently than before.
Since we are not dealing with Java GUIs in this practice, we will learn about them through another example, namely collections. First, we'll traverse a list (but it would work the same way on a set), and then a Map object consisting of key-value pairs.
The syntax of a lambda expression is (parameter1, parameter2) -> statement, or statement block
. You don't have to write out the type of the parameters (but you can if you want). For one parameter, you can omit the parentheses around the parameter.
public class Main {
public static void main(String[] args) {
List<String> colors = new ArrayList<>();
colors.add("Blue");
colors.add("Green");
colors.add("Red");
colors.add("Black");
colors.add("Yellow");
colors.add("Orange");
colors.forEach(color -> System.out.println(color));
}
}
You can see how much easier it is to use than, for example, a traditional for loop. If you use multiple statements, you have to put the statements between brackets after the arrow(->
) in the usual way.
public class Main {
public static void main(String[] args) {
List<String> colors = new ArrayList<>();
colors.add("Blue");
colors.add("Green");
colors.add("Red");
colors.add("Black");
colors.add("Yellow");
colors.add("Orange");
colors.forEach(color -> {
if (color.charAt(0) > 'O') {
System.out.println(color);
}
});
}
}
In the above example, we'll go through the list and see which colors start with a letter after an 'O' and write them to the default output. In our current situation, this might not seem like a big deal, because we could have traversed the list with a plain iterator or a for loop, with about the same amount of code.
However, let's look at this traversal in the case of a Map, where we have a noticeable simplification. (Not to mention the event handler for the GUI elements we saw in the lecture.)
public class Main {
public static void main(String[] args) {
Map<String, Integer> colors = new HashMap<>();
// We have asked the favorite color of 1000 people and we want to store it
// in this map.
colors.put("Blue", 320);
colors.put("Green", 200);
colors.put("Yellow", 80);
colors.put("Brown", 95);
colors.put("Lemon", 105);
colors.put("Red", 75);
colors.put("Purple", 125);
colors.forEach((color, value) -> System.out.println(color + " color is " + value + " people's favorite."));
}
}
public class Main {
public static void main(String[] args) {
Map<String, Integer> hashes = new HashMap<>();
// We have asked the favorite color of 1000 people and we want to store it
// in this map.
colors.put("Blue", 320);
colors.put("Green", 200);
colors.put("Yellow", 80);
colors.put("Brown", 95);
colors.put("Lemon", 105);
colors.put("Red", 75);
colors.put("Purple", 125);
colors.forEach((color, value) -> {
if (value > 100) {
System.out.println(color + " color is " + value + " people's favorite.");
} else {
System.out.println(color + " color is not the favorite of many people.");
}
});
}
}
It can be seen that, unlike the above, lambda expressions can be used to traverse a map in a very simple and transparent way. Hopefully everyone is interested in learning more about lambdas, we recommend the following links:
Assignments¶
1 Write a program in which you can read and save pgm files and perform all kinds of transformations on them. Try to split the task into parts, i.e. don't try to implement everything in a single class! + Advanced extra: Create a class in your program that can display pgm files!
Example input files: torony-o.pgm, rektori.pgm.