Core Java. Лекция 5

Строки. Регулярные выражения. Wrapper classes. IO classes. DateTime API

Иван Пономарёв, КУРС/МФТИ

Мы делаем паузу в изучении синтаксиса языка ради изучения библиотеки

  • J. Bloch, 'Effective Java': 'By using a standard library, you take advantage of the knowledge of the experts who wrote it and the experience of those who used it before you…​ Numerous features are added to the libraries in every major release, and it pays to keep abreast of these additions.'

  • Наряду с актуальными, «свежими» классами в библиотеках есть много старых классов, оставленных для обратной совместимости.

  • Вы встретите Vector, Date, File, StringBuffer, Random, о которых я не буду рассказывать на лекциях.

  • Будьте внимательны: использование устаревших классов в существующем коде, в примерах в интернете и т. п. не оправдывает их использование в новом коде.

Отдельный пример из книги Effective Java: ThreadLocalRandom

//Плохо: сид выбирается при каждом вызове. НЕТ!!
double rnd = (new Random()).nextDouble();

//До Java 7+ так было нормально, но теперь НЕТ!!
static final Random r = new Random();
static double random() {
  return r.nextDouble();
}

//Java 7-16: в 3.6 раз быстрее, проще, качественнее
double rnd = ThreadLocalRandom.current().nextDouble();

//Java 17+: JEP356. Not thread safe!
RandomGenerator generator = RandomGenerator.getDefault();
generator.nextDouble();

Класс String

String e = ""; // an empty string
String java = "Java\u2122"; //Java™

Строка — иммутабельный объект.

Нужна другая строка? Сооружаем новую:

String greeting = "Hello!"
greeting = greeting.substring(0, 3) + "p!"; //Help!

Хотя внутри строки — массив, изменить его отдельные элементы нельзя!

Внутреннее представление строк

Пул строковых констант

String name1 = "John Doe";
String name2 = "John Doe";
/* DO NOT DO THIS!!*/
String name3
  = new String("John Doe");
stringpool

Сравнение строк

  • if (a == "John Doe") — неправильно, ошибка новичка.

  • if (a.equals("John Doe")) — плохо, получим NPE, если a == null.

  • if ("John Doe".equals(a)) — так делают серьёзные ребята.

  • if ("John Doe".equalsIgnoreCase(a)) — сравнение без учёта регистра.

  • if(str != null && !str.isEmpty()) — в этой строке что-то есть!

  • if(str != null && !str.isBlank()) — в этой строке что-то есть помимо пробелов, табуляций, переносов и проч.!

Интернирование строк

String name1 = "John Doe";
String name2 =
  new String("John Doe");
String name3 =
  new String("Jane Roe");
stringpool intern1

Интернирование строк

name1 = name1.intern();
name2 = name2.intern();
name3 = name3.intern();
stringpool intern2

Конкатенация строк

String foo = "foo";
String bar = "bar";

System.out.println(foo + bar); //foobar

Ситуация такова:

  • Пользовательской перегрузки операторов в Java не предусмотрено.

  • Оператор + для строк перегружен на уровне языка.

  • Оператор == для строк не перегружен, хоть его неперегруженная версия для строк и не имеет смысла :-(

​Конкатенация строк: проблема производительности​

//Плохая производительность, не делайте так!
String result = "";
for (int i = 0; i < numItems(); i++)
  result += lineForItem(i);
return result;

​Конкатенация строк: используйте StringBuilder!​

//Заранее можно аллоцировать приблизительное кол-во символов
StringBuilder b = new StringBuilder(numItems() * LINE_WIDTH);
//А можно и по умолчанию: new StringBuilder() (16 chars)
for (int i = 0; i < numItems(); i++)
  b.append(lineForItem(i));
return b.toString();

Длина строки

Длина строки в UTF-16 code units:

String greeting = "Hello";
int n = greeting.length(); // is 5.

«Реальная» длина строки в code points:

int cpCount = greeting.codePointCount(0, greeting.length());

Символ на i-й позиции

//работает за время O(1)
char first = greeting.charAt(0); // first is 'H'
char last = greeting.charAt(4); // last is 'o'

Если нужны code points:

int[] codePoints = str.codePoints().toArray();

Полезные методы

int length()
boolean isEmpty() / boolean isBlank()
char charAt(int index)
int compareTo(String anotherString)
boolean equals(Object anObject)
boolean equalsIgnoreCase(String anotherString)
boolean startsWith(String prefix)
boolean endsWith(String suffix)
String toLowerCase() / String toUpperCase()
String trim()
String join(CharSequence delimiter, CharSequence... elements)

Вредные методы

  • indexOf.. / lastIndexOf..

  • replace..

  • split..

В комбинации c substring и конкатенацией их используют для извлечения данных / преобразования строк.

Не делайте так. Это путь к бесконечным багам, бесконечной боли и бесконечному стыду. Используйте конечные автоматы или регулярные выражения.

Regular expressions

String input = "11:59 am";
Pattern pattern =
  Pattern.compile("([1-9]|1[0-2])\\s*:\\s*([0-5][0-9])\\s*([ap]m)");
Matcher matcher = pattern.matcher(input);

//matches() requires full match for the whole string,
//find() -- finds the first match
if (matcher.matches()) {
    String hours = matcher.group(1); //11
    String minutes = matcher.group(2); //59
    String period = matcher.group(3); //am
}

Find/replace in a loop

An example from StackOverflow: we want "1xxxx23" as an output:

Map<String, String> replacements = new HashMap<String, String>();

replacements.put("${env1}", "1");
replacements.put("${env2}", "2");
replacements.put("${env3}", "3");

String line ="${env1}xxxx${env2}${env3}";

Find/replace in a loop

//Before Java9 StringBuffer was used
StringBuilder sb = new StringBuilder();

Pattern p = Pattern.compile("(\\$\\{[^}]+\\})");
Matcher m = p.matcher(line);
while (m.find()) {
    String repString = replacements.get(m.group(1));
    if (repString != null)
        m.appendReplacement(sb, repString);
}
m.appendTail(sb);

System.out.println(sb.toString()); //1xxxx23
  • Для интерполяции строк в реальной практике используют класс StringSubstitutor из библиотеки org.apache.commons:commons-text.

Regular expressions

  • Advantages

    • Can validate string format

    • Tolerable to optional separators, 'garbage' etc.

    • Groups are powerful device for data extraction

  • The price we pay

    • You have to learn (there are thick books on regular expressions)

    • You have to learn to use them correctly (you can easily shatter the performance)

    • They look horribly and perplexing

State machines

How can we parse the following string?

  Name,Goods and Services
  LLC "Monkey Business","stuff, things"
  "LLC ""Sea Shore Shop""",sea shells

Definite State Machine!

dfa

Implementation

enum State {START, QUOTED_BODY, QUOTE_READ, UNQUOTED_BODY}
List<String> parse(String line) {
  State state = State.START;
  List<String> result = new ArrayList<>();
  StringBuilder fieldValue = new StringBuilder();
  for (char c : line.toCharArray()) {
    switch (state) {
        case START -> { . . .  }
        case QUOTED_BODY -> { . . .  }
        case QUOTE_READ -> { . . . }
        case UNQUOTED_BODY -> { . . . }
    }
  }
  result.add(fieldValue.toString());
  return result;
}

Started to read the field

start
case START -> {
    fieldValue = new StringBuilder();
    if (c == '"')
        state = State.QUOTED_BODY;
    else {
        fieldValue.append(c);
        state = State.UNQUOTED_BODY;
    }}

Quoted value

quoted body
case QUOTED_BODY -> {
    if (c == '"')
        state = State.QUOTE_READ;
    else {
        fieldValue.append(c);
    }}

Came across a quote mark

quote read
case QUOTE_READ -> {
    if (c == '"') {        fieldValue.append(c);
                           state = State.QUOTED_BODY;
    } else if (c == ',') { result.add(fieldValue.toString());
                           state = State.START;
    } else {               //invalid input format
                           throw new IllegalStateException();
    }}

Unquoted field

unquoted body
case UNQUOTED_BODY -> {
    if (c == ',') { result.add(fieldValue.toString());
                    state = State.START;
    } else {        fieldValue.append(c);
    }}

Conclusion

  • Математический факт: Каждый ДКА может быть сконвертирован в регулярное выражение, и наоборот.

  • При обработке строк регулярки и ДКА — наше всё.

  • (Ну а если надо работать с CSV-файлами, для этого есть библиотека org.apache.commons:commons-csv.)

Wrapper Types

Primitive types are incompatible with objects. So there are Wrapper Classes:

byte, short, int, long

Byte, Short, Integer, Long

char

Character

float, double

Float, Double

boolean

Boolean

Wrapper-types are similar to strings

  • Объекты иммутабельные. Если нужен новый — делаем новый объект, а не модифицируем старый.

  • Проверка равенства — только по equals!

  • Подобно String Pool-у, эти объекты кэшируются, поэтому лучше их следует создавать через valueOf (публичные конструкторы, позволяющие создание через new, помечены как @Deprecated).

  • Начиная с Java 28, все типы-обёртки становятся value-типами.

Object pool (caching)

Integer five1 = Integer.valueOf(5);
Integer five2 = Integer.valueOf(5);
/* DO NOT DO THIS!!*/
Integer five3
  = new Integer(5);
ipool

What is cached

  • Integer, Short, Long — -128..127

  • 'Byte' — completely (-128..127)

  • Character — 0..127

  • 'Boolean' — 'Boolean.TRUE' and 'Boolean.FALSE'

  • 'Float', 'Double' — are not cached, although in the JavaDoc for valueOf the possible caching is mentioned.

  • Truth of Life: You’ll use autoboxing more often than valueOf.

Autoboxing & unboxing

ArrayList<Integer> list = new ArrayList<>();

//autoboxing :-)
list.add(3);
//without autoboxing :-(
list.add(Integer.valueOf(3));

//unboxing :-)
int n = list.get(i);
//with no automatic unboxing :-(
int n = list.get(i).intValue();

Autoboxing is not always that obvious

Integer n = 3;
//unbox - increment - box.
//not so effective!!
n++;

Integer a = 1000;
Integer b = 1000;
if (a == b) . . . //NO! THIS WILL FAIL!

Convert a number from and to a string

Static methods:

  • Integer.parseInt(value, radix)

  • Integer.toString(value, radix)

  • Double.parseDouble(value)

  • Double.toString(value)

Класс BigInteger

BigInteger i = new BigInteger("123456789012345678901234567890");
BigInteger j = new BigInteger("123456789012345678901234567892");

BigInteger sum = i.add(j);
BigInteger difference = i.subtract(j);
BigInteger quotient = i.divide(j);
BigInteger product = i.multiply(j);

BigDecimal class

public static BigDecimal calculateTotalAmount(
      BigDecimal quantity,     BigDecimal unitPrice,
      BigDecimal discountRate, BigDecimal taxRate) {
    BigDecimal amount = quantity.multiply(unitPrice);
    BigDecimal discount = amount.multiply(discountRate);
    BigDecimal discountedAmount = amount.subtract(discount);
    BigDecimal tax = discountedAmount.multiply(taxRate);
    BigDecimal total = discountedAmount.add(tax);

    // round to 2 decimal places using HALF_EVEN
    BigDecimal roundedTotal = total.setScale(2, RoundingMode.HALF_EVEN);
    return roundedTotal;
}

Input/Output

ibm2741

IBM2741 terminal, 1965 (source: Wikipedia)

Basic I/O streams

iostreams

Combining streams

combining
Path path = Paths.get("path/to/file.dat");

try (DataInputStream dis =
         new DataInputStream(
             new BufferedInputStream(
                 Files.newInputStream(path)))) {
    double doubleVal = dis.readDouble();
    long longVal = dis.readLong();
}

Combining Streams: Reading a Zip File

Path path = Paths.get("path/to/file.zip");

try(ZipInputStream zis =  new ZipInputStream(
    Files.newInputStream(path));
    DataInputStream dis = new DataInputStream(zis)) {
    . . .
}

Read/write text data

The modern, simple and correct method:

Path path = Paths.get("path/to/file.txt");

//Lazy reading
Stream<String> lines = Files.lines(path, StandardCharsets.UTF_8);

//Вычитка всего содержимого в память
List<String> strings =
  Files.readAllLines(path, StandardCharsets.UTF_8);

Using BufferedReader

try (BufferedReader br =
     Files.newBufferedReader(path, StandardCharsets.UTF_8)){
    String line;
    while ((line = br.readLine())!=null){
           . . .
    }
}

Save prepared data to text

Path path = Paths.get("path/to/file.txt");
List<String> lines = new ArrayList<>();
lines.add(...)

//Если всё уже готово в памяти
Files.write(path, lines, StandardCharsets.UTF_8);

Saving the text data incrementally

try (PrintWriter printWriter = new PrintWriter(
                Files.newBufferedWriter(
                  path, StandardCharsets.UTF_8))) {
            printWriter.println(...);
            printWriter.printf(...);
        }

Working with the file system

  • Obsolete File (can still be found, in particular, as an argument of the FileInputStream constructor).

  • More modern Path (Java 7+)

Working with the file system

//Path не обязательно указывает
//на уже существующий файл или каталог!!

Path absolute = Paths.get("/home", "harry");
Path relative = Paths.get("myprog", "conf", "user.properties");

Path subPath = absolute.resolve("file.txt");

subPath.getParent(); //parent
subPath.getFileName(); //the last component of the path

Working with the file system

//all components except the last component must exist
Files.createDirectory(path);
creates all directories in path
Files.createDirectories(path);
creates an empty file
Files.createFile(path);

//creates a temporary file in an OS-specific location
Files.createTempFile(prefix, suffix);

File verification

//it makes sense to check before you open a stream for reading
Files.exists(path)
Files.isReadable(path)

Files.size(path)

Getting the list of files and depth-first directory traversal

//the contents of the folder (without going to subfolders)
Stream<Path> list = Files.list(path);

//метод для обхода дерева папок
Stream<Path> Files.walk(Path start,
   int maxDepth, FileVisitOption... options)

Copy, move and delete files

Files.copy(fromPath, toPath)

Files.move(fromPath, toPath)

Files.delete(path)

Clocks and Datetime API

How many seconds are there in a day?

persistenceofmemory

System clock

  • long System.currentTimeMillis() — количество миллисекунд, прошедшее с наступления полуночи 1 января 1970 года на Гринвичском меридиане минус число високосных секунд, с точки зрения системных часов.

    • Гранулярность может быть больше миллисекунды.

    • «Замирают» при вставке високосной секунды.

    • Значения могут «скакать» в обе стороны при синхронизации через NTP!

  • long System.nanoTime() — монотонные часы.

    • Наносекундная точность.

    • Нет привязки к «календарному» времени.

    • На больших периодах может быть заметна неточность хода часов.

When to use what?

  • long System.nanoTime() — при замерах периодов времени в переделах одной машины.

    • Но для замеров производительности программ на Java надо применять JMH.

  • long System.currentTimeMillis() — для генерации штампов времени (с пониманием ограничений).

DateTime API

  • Obsolete, inconvenient, mutable, non-currently safe classes (do not use them):

    • Java 1.0 'Date' (it’s still with us because of JDBC),

    • Java 1.1 Calendar.

    • 'DateFormat' works with them.

  • Modern (Java 8+), immutable classes, learned from mistakes:

    • LocalDate, LocalTime, Instant, Duration, Period

    • ZoneId, ZonedDateTime.

    • 'DateTimeFormatter' works with them.

Instant

  • Point on time line

  • UNIX time

//Current instant
Instant.now();
//explicitly specified Instant
Instant.ofEpochSecond(3);
//What year is it today?
int year = Instant.now().get(ChronoField.YEAR);

LocalDate, LocalTime and LocalDateTime

  • Date and time in the "human" sense.

  • Without information about time zone → without reference to 'Instant'.

  • Often binding to an Instant is not needed: "weekly call at 9:00" bound to Instant will "creep" due to daylight saving time.

LocalDate date = LocalDate.of(2014, 3, 18); // March 18, 2014

int year      = date.getYear(); //2014
Month month   = date.getMonth();  //MARCH
int day       = date.getDayOfMonth(); //18
DayOfWeek dow = date.getDayOfWeek(); //TUESDAY

LocalTime and LocalDateTime

LocalTime time = LocalTime.of(13, 45, 20); //13:45:20

int hour   = time.getHour();
int minute = time.getMinute();
int second = time.getSecond();

LocalDateTime dt1 = LocalDateTime.of(2014, Month.MARCH, 18, 13, 45, 20);
LocalDateTime dt1 = LocalDateTime.of(date, time);
LocalDateTime dt2 = date.atTime(time);
LocalDateTime dt3 = time.atDate(date);

dt1.toLocalDate(); //2014-03-18
dt1.toLocalTime(); //13:45:20

Duration and Period

  • Duration: количество времени между двумя точками на временной прямой с наносекундной точностью.

  • Period: разница во времени между LocalDate в днях (например, «сколько дней действует договор?»)

  • Оба реализуют интерфейс TemporalAmount.

//В качестве аргументов годятся LocalTime, LocalDateTime и Instant
Duration d = Duration.between(time1, time2);

//Only LocalDate is suitable as an argument
Period tenDays = Period.between(
    LocalDate.of(2014, 3, 8),
    LocalDate.of(2014, 3, 18));

Constructing new objects from old ones

Instant i = Instant.now();
Duration d = Duration.ofSeconds(3);

//For example:
i.plus(dd.multipliedBy(10))
 .minus(dd.minus(Duration.ofMillis(1)));

//the same can be done with LocalDateTime

Format and parse date and time

DateTimeFormatter formatter1 =
  DateTimeFormatter.ISO_LOCAL_DATE; // YYY-MM-DD
DateTimeFormatter formatter2 =
  DateTimeFormatter.ofPattern("dd/MM/yyyy");
DateTimeFormatter formatter3 = new DateTimeFormatterBuilder()
  .appendText(ChronoField.DAY_OF_MONTH)
  .appendLiteral(". ")
  .appendText(ChronoField.MONTH_OF_YEAR)
  .appendLiteral(" ")
  .appendText(ChronoField.YEAR)
  .parseCaseInsensitive()
  .toFormatter(Locale.ITALIAN);

To string and from string

LocalDate date = LocalDate.of(2014, 3, 18);
String s = date.format(
  DateTimeFormatter.ISO_LOCAL_DATE); // 2014-03-18
LocalDate date2 = LocalDate.parse(
  s, DateTimeFormatter.ISO_LOCAL_DATE);

Time zones

ZoneId moscowZone = ZoneId.of("Europe/Moscow");
ZoneId romeZone = ZoneId.of("Europe/Rome");
LocalDateTime dateTime = LocalDateTime.now();

ZonedDateTime moscowTime = dateTime.atZone(moscowZone);
System.out.println(moscowTime);
//2019-09-30T18:15:52.670+03:00[Europe/Moscow]

ZonedDateTime romeTime =
  moscowTime.withZoneSameInstant(romeZone);
System.out.println(romeTime);
//2019-09-30T17:15:52.670+02:00[Europe/Rome]