Все классы в Java наследуют от некого классаObject.
В этом классе определены некоторые важные методы, которые необходимо знать и самое главное уметь переопределять их.
Все классы как уже было сказано наследуют отObject, а значит они наследуют и все методы, которые определены в нем, а значит наследники могут ими пользоваться и переопределять их.
Рассмотрим самые важные.
Первым рассмотрим toString.
По умолчанию метод toString возвращает хеш-код объекта. Хеш-код мы затронем позже.
Но информация о хеш-коде объекта не особо полезна, поэтому принято переопределять метод toString так чтобы toString возвращал всю полезную информацию об объекте (содержимое его полей).
Пример программы:
class MyClass {
int myA;
Integer myB;
MyClass(int myA, int myB) {
this.myA = myA;
this.myB = myB;
}
// toString – предоставление информации об объекте.
// Как видим ниже, он возвращает строку
// со значениями полей объекта,
// то есть это вся необходимая информация о нем.
// Переопределять этот метод желательно именно
// в формате вот такой строки.
// Причем программисту даже не нужно будет писать
// имя метода toString для его вызова –
// этот метод будет вызываться автоматически, если
// попытаться передать объект в println,
// вот так: System.out.println(someobject);
// где someobject – объект класса MyClass.
// Смотри пример ниже в main.
@Override
public String toString() {
return “MyClass{” + “myA=” + myA + “, myB=” + myB + ‘}’;
}
}
public class ToStringLesson {
public static void main(String[] args)
throws CloneNotSupportedException {
MyClass someobject = new MyClass(10, 20);
// Как можно увидеть, в консоли вывело ту самую
// строку с полями объекта someobject.
// При этом, как уже говорилось, нам не пришлось
// вызывать метод toString вот так someobject.toString();
System.out.println(someobject);
}
}
File – класс для работы с элементами файловой системы. С помощью различных методов этого класса можно получить информацию о переданном в конструктор объекта этого класса элементе файловой системы.
Например:
getPath() – вернуть путь переданного элемента,
getName() – вернуть имя переданного элемента,
isDirectory() – является ли директорией.
Пример программы:
import java.io.*;
import java.util.*;
class FileExample {
public static void main(String[] args) {
// Для примера выведем полный путь к файлу person.txt
// в текущей директории с помощью метода getAbsolutePath.
File file = new File(“person.txt”);
System.out.println(file.getAbsolutePath());
// или например проверим является ли переданное
// в конструктор директорией с помощью метода isDirectory.
// Вернет true, так как InputOutputStreams
// является директорией.
File directory = new File(
“C:/Users/User/Desktop/Java/JavaExamples/InputOutputStreams”
);
System.out.println(directory.isDirectory());
}
}
Вывод:
Как видим, вернулся абсолютный путь файла person.txt и то, что InputOutputStreams является директорией.
Также есть много других полезных методов для создания, удаления или переименования файла или директории, проверки готов ли файл для записи, получить размер файла.
Все эти методы, конечно, приводить не будем, так как они очень простые.
Далее разберем некоторые чуть более сложные и полезные.
Метод listFiles.
С помощью метода listFiles можем получить все файлы в директориии поместить их в массивFile[]. Благодаря чему можем пройтись по всем файлам с помощью цикла и выполнить какие-то действия с каждым из них.
Пример программы:
import java.io.*;
import java.util.*;
class FileExample {
public static void main(String[] args) {
File directory = new File(
“C:/Users/User/Desktop/Java/JavaExamples/InputOutputStreams”);
System.out.println(directory.isDirectory());
// Получим все файлы в директории InputOutputStreams
File[] files = directory.listFiles();
// Каждый элемент массива files — это один файл из InputOutputStreams.
for (File f : files) {
// Выведем имена каждого файла в директории InputOutputStreams
System.out.println(f.getName());
}
}
}
Вывод:
В консоль вывелись имена всех файлов в папке InputOutputStreams.
Класс FileFilter.
FileFilter предназначен для проверки попадает ли объект File под некоторое условие.
Созданный объект FileFileter обычно передается в метод listFiles для фильтрации файлов в деректории.
Пример программы:
import java.io.*;
import java.util.*;
class FileExample{
public static void main(String[] args) {
File directory = new File(“C:/Users/User/Desktop/Java/JavaExamples/InputOutputStreams”);
// Нужно переопределить файл accept, который
// будет применяться к каждому файлу, который будет
// выбираться из директории с помощью listFiles
FileFilter ff = new FileFilter() {
// Анонимным классом переопределяем accept
public boolean accept(File file) {
// Если файл в директории заканчивается на .txt
if (file.getName().endsWith(“.txt”)) {
// Возвращаем true
return true;
}
// Иначе false
return false;
}
};
// Метод listFiles(FileFilter filter) отбирает не все
// файлы данного каталога, а только те, которые
// удовлетворяют определенному условию.
// Параметр filter предназначен для
// задания этого условия.
// Добавляет в массив файлов только те, accept
// которых вернул true
File[] files = directory.listFiles(ff);
for (File f: files) {
System.out.println(f.getName());
}
}
}
Вывод:
Как видим, вывелись на консоль только имена файлов с форматом.txt.
Избавляет от необходимости вручную реализовывать операции над коллекциями.
Проще говоря пишем запрос к коллекции как к базе данных без ничего лишнего.
То есть например мы можем запросить из коллекции только четные числа простым запросом в формате цепочки соответствующих функций класса Stream.
Также позволяет производить операции над элементами коллекции в параллельных потоках.
Пример программы:
import java.util.*;
import static java.util.stream.Collectors.toList;
class SStream{
public static void main(String[] args) {
List< Integer > numbers = Arrays.asList(3,2,2,3,7,3,5);
// map нужен для преобразования элементов по какому-то правилу. Например
// с помощью map умножаем каждый элемент коллекции сам на себя.
// То есть i здесь это элемент коллекции и stream ниже проходит по каждому
// элементу коллекции и совершает с ним операцию, заданную нами с помощью
// лямбда-выражения. В нашем случае — умножение элемента i самого на себя.
// В collect передаем метод, который будет преобразовывать в необходимую
// коллекцию результат запроса. В нашем случае результирующая коллекция
// будет List, так как метод toList().
// В итоге выводимая на консоль коллекция List будет содержать числа,
// которые являются результатом лямбда-выражения.
System.out.println(numbers.stream().map(i -> i*i).collect(toList()));
// filter — фильтрует элементы в соответствии с условием.
// Здесь запрашиваем только четные числа коллекции.
System.out.println(numbers.stream().filter(i -> i%2 == 0).collect(toList()));
// limit — оставляет только указанное количество элементов коллекции.
System.out.println(numbers.stream().limit(4).collect(toList()));
// forEach — применяем какое-то действие к каждому элементу.
numbers.stream().forEach(System.out::println);
// sorted — сортирует элементы. Также может принимать аргументом лямбда-
// выражение, по которому сортирует.
System.out.println(numbers.stream().sorted().collect(toList()));
// parallel — для распараллеливания выполнения операций с элементами коллекции.
// То есть благодаря добавлению parallel к коллекции лямбда-выражение может
// выполняться намного быстрее — и насколько быстро, зависит от количества ядер
// в компьютере.
System.out.println(numbers.stream().parallel().map(i -> i*i).collect(toList()));
}
}
Всё просто. Это просто сокращенная форма реализации функц. интерфейса с помощью анонимного класса.
Реализовывать абстрактный метод функционального интерфейса с помощью анонимного класса не очень лаконично.
Поэтому лучше использовать лямбда выражения.
Пример программы:
import java.util.*;
interface Adder{
public double pow(double a, double b);
}
class Lambda{
public static double func(Adder a){
return a.pow(2, 5);
}
public static void main(String[] args) {
/* Вместо:
System.out.println(func(new Adder() {
public double pow(double a, double b){
return Math.pow(a, b);
}
})); */
// Проще сделать так:
// Здесь a1, a2 это аргументы единственного
// метода в функц. интерфейсе. После стрелочки
// тело(реализация) функции единственного
// метода в функц. интерфейсе.
// То есть (a1, a2) это (double a, double b),
// а {return Math.pow(a1, a2);} это собственно
// реализация pow
System.out.println(func((a1, a2)->{return Math.pow(a1, a2);}));
// Еще проще через ссылку на метод(сокращенная
// форма лямбда выражения вызывающего только
// определенный метод)
System.out.println(func(Math::pow));
}
}
В прошлом уроке мы видели интерфейс с одним единственным абстрактным методом.
Такой интерфейс называется функциональным.
Функциональный интерфейс – представляет одну абстрактную функцию, которая существует для ее переопределения анонимными классами или лямбда выражениями (позже разберем что это).
Открывает функциональное программирование. То есть функция используется как объект (ее можно хранить как объект, передать как объект аргументом в метод и т.д).
Пример программы:
import java.util.*;
interface Adder{
public double pow(double a, double b);
}
//Разъясним на примерах что такое
//функциональное программирование.
class FuncInt{
public static void main(String[] args) {
//В прошлом уроке мы не создавали ссылку
//на однoразовый объект анонимного класса
//в этом уроке создадим ее (adder).
Adder adder = new Adder() {
public double pow(double a, double b){
double result = a;
for(int i=1; i < b; i++){
result *= a;
}
return result;
}
};
//Теперь созданный объект
//не такой однoразовый как в прошлом уроке
//так как мы создали ссылку adder и можем работать
//с созданным объектом через нее.
//Например вызовем метод pow через эту ссылку.
System.out.println(adder.pow(3,6));
//И где же здесь функциональное программирование?
//Спросите вы. В начале было упомянуто что
//"функцию можно хранить как объект".
//Что это значит? Это значит что мы сохранили
//реализацию функции pow в объект, к которому мы
//можем получить доступ через ссылку adder.
//То есть мы фактически храним одну единственную
//функцию в одном объекте. и мы можем пользоваться
//этой функцией через этот объект. и эта
//единственность функции достигается как раз
//функциональным интерфейсом поскольку в нем может
//быть всего одна функция. Спросите чем же так важна
//эта единственность? На этот вопрос лучше всего
//ответить аналогией. Мы же не храним
//в переменной int два числа мы храним одно.
//То есть например int a=5,b=59,c=91.
//Каждая переменной хранится одно число и
//также мы храним одну функцию в одном объекте.
//В adder мы сохранили одну реализацию метода pow,
//также можем создать adder1, в котором мы сохраним
//другую реализацию метода pow.
//Например ниже
Adder adder1 = new Adder() {
//другая реализация метода pow
//с помощью библиотеки Math.
public double pow(double a, double b){
//возвращаем a возведенное в степень b
return Math.pow(a, b);
}
};
}
}
Вывод:
Выше было упомянуто что функцию можно передать как обьект аргументом в метод.
Продемонстрируем это.
Пример программы:
import java.util.*;
interface Adder{
public double pow(double a, double b);
}
class FuncInt1{
// Создадим какую-нибудь функцию, в которой будет
// использоваться метод pow из функционального
// интерфейса Adder.
public static double func(Adder a){
return a.pow(2, 5) + 20;
}
public static void main(String[] args) {
Adder adder = new Adder() {
public double pow(double a, double b){
double result = a;
for(int i = 1; i < b; i++){
result *= a;
}
return result;
}
};
Adder adder1 = new Adder() {
public double pow(double a, double b){
return Math.pow(a, b);
}
};
// Очевидно, что мы можем передать функцию
// в объекте adder или adder1 в метод func,
// чтобы она там, в func, использовалась.
System.out.println(func(adder));
// или
System.out.println(func(adder1));
// То есть мы видим, что можем передать
// в метод именно функцию, как например
// числовую переменную или как сложный объект,
// но мы передаем именно одну функцию.
// И думаю, очевидно, почему программирование
// называется функциональным:
// Мы оперируем функциями.
}
}
Анонимный класс – одноразовая реализация интерфейса без создания полноценного класса.
То есть нам не нужно создавать много объектов, будет только один с данной реализацией.
Пример программы:
import java.util.*;
interface Adder{
public double pow(double a, double b);
}
class AnonimousClass{
public static void main(String[] args) {
// Как видим, мы передаем в метод println
// объект на основе одноразовой реализации
// интерфейса Adder и сразу пользуемся
// этой реализацией, вызывая у этого объекта
// только что реализованный pow
// (то есть вызываем у реализованного
// обьекта .pow(3,6)).
System.out.println(new Adder() {
public double pow(double a, double b){
double result = a;
for(int i = 1; i < b; i++)
result *= a;
return result;
}
}.pow(3,6));
}
}
Вывод:
Одноразовый он очевидно почему.
Потому что мы не создавали полноценный отдельный класс реализующий интерфейс, как мы делали это раньше. И мы даже не создавали ссылку для работы с созданным объектом, мы сразу вызвали у него .pow(3,6).
То есть вся эта реализация существовала для одной цели – вызова метода .pow(3,6) в методе println и всё. И больше нигде мы эту реализацию использовать не можем да и не должно быть нужно, она одноразовая.
В java 8 всё же появилась возможность добавлять реализации методов в интерфейс.
Нужно в основном для того чтобыу классов реализующих интерфейс не было слишком много одинаковых реализацийабстрактных методов интерфейса.
До java 8 приходилось делать так:
import java.util.*;
interface Car {
public void gas();
public void brake();
}
class Sedan implements Car {
@Override
public void gas() {
System.out.println(“Gas!”);
}
@Override
public void brake() {
System.out.println(“Stop!”);
}
}
class Truck implements Car {
// такая же как в прошлой реализации
@Override
public void gas() {
System.out.println(“Gas!”);
}
// такая же как в прошлой реализации
@Override
public void brake() {
System.out.println(“Stop!”);
}
}
class PickUp implements Car {
// такая же как в прошлой реализации
@Override
public void gas() {
System.out.println(“Gas!”);
}
// такая же как в прошлой реализации
@Override
public void brake() {
System.out.println(“Stop!”);
}
}
// Как можно увидеть у всех реализаций
// интерфейса Car одинаковые реализации gas и brake
// что очевидно плохо
class Ddefault {
public static void main(String[] args) {
Sedan sedan = new Sedan();
Truck truck = new Truck();
PickUp pickUp = new PickUp();
sedan.gas();
truck.gas();
pickUp.gas();
}
}
Вывод:
Добавив дефолтные реализации gas и brake в интерфейс всем классам, которые реализуют Car больше не придется делать одинаковые реализации этих методов.
После java 8:
import java.util.*;
// Определение интерфейса Car
interface Car {
// Дефолтные реализации методов.
// Помечаются словом default
public default void gas() {
System.out.println(“Gas!”);
}
public default void brake() {
System.out.println(“Stop!”);
}
}
// Классы, реализующие интерфейс Car
class Sedan implements Car {
}
class Truck implements Car {
}
class PickUp implements Car {
}
class Ddefault {
public static void main(String[] args) {
// Создание объектов классов
Sedan sedan = new Sedan();
Truck truck = new Truck();
PickUp pickUp = new PickUp();
// Вызов метода gas() для каждого объекта
sedan.gas();
truck.gas();
pickUp.gas();
}
}
Ключевое слово assert используется для проверки какого-либо условия в ходе разработки.
Программа выдает исключение, то есть происходит ошибкаесли выражение в условии после ключевого слова assert возвращает false.
В итоговом коде не присутствует, так как используется для простого тестирования условий.
Запускается с ключом -ea.
Поясним на примере:
import java.io.*;
public class Assert {
public static void main(String args[]) {
int n=5;
for(int i=0; i < 10; i++) {
n--;
assert n > 0; //произойдет сбой когда n дойдет до нуля
System.out.println(“n equals ” + n);
}
}
}
Вывод:
Как видим, когда выполнение программы доходит до того, что n становиться равным 0 происходит ошибка.
Есть специальные классы обертки – Integer, Char, Float.
Они также как и простые типы значений хранят в себе одно простое значение: Integer – целое число, Char – один символ и т.д.
Зачем же они нужны?
Обычные типы значений не могут иметь значение null, то есть пустота. Обертки же могут.
Также они нужны чтобы указывать тип хранимых значений в коллекциях (коллекции будут рассмотрены позже).
Поясним на примере:
public class Wrappers {
public static void main(String[] args) {
Integer integer = null;
// int num = null; – не скомпилируется
System.out.println(integer);
// запишем просто целое число
integer = 7;
System.out.println(integer);
}
}
Вывод:
Также важно знать что обертки хоть и являются классами и могут иметь объекты, но они не являются ссылочными, то есть ведут они себя также как типы значений.
Для хранения строки мы знаем тип String. Но он не всегда бывает эффективен.
Поэтому есть еще другие классы для хранения строк и взаимодействия с ними, которые нужно знать.
Поясним на примере:
public class StringTypes {
public static void main(String[] args) {
// Создадим String переменную
String output = “Some text”;
// и добавим в конец этой строки единицу и пробел сто раз.
int count = 100;
for(int i = 0; i < count; i++) {
output += i + " "; // При каждой итерации цикла
// создается НОВЫЙ объект String с очередным добавленным i.
// Просто представьте, как это плохо скажется на памяти
}
System.out.println(output);
}
}
Вывод:
Поэтому существует StringBuffer, который решает эту проблему.
public class StringTypes {
public static void main(String[] args) {
int count = 100;
// Поэтому существует StringBuffer, который решает эту проблему
StringBuffer output = new StringBuffer(110);
// append – добавить в объект StringBuffer текст
output.append(“Some text”);
for(int i = 0; i < count; i++) {
output.append(i + " "); // Теперь при каждой итерации
// работаем всего с одним экземпляром StringBuffer
// Профит!
}
System.out.println(output.toString());
}
}
Вывод:
Также есть StringBuilder – то же самое, что и StringBuffer только потокоНЕбезопасен, поэтому работает быстрее