Lecture
The whole half-century history of computer programming, and perhaps the history of all science, is an attempt to cope with the complexity of the surrounding world. The tasks facing programmers are becoming increasingly cumbersome, the information that needs to be processed grows like a snowball. More recently, the usual units of information measurement were kilobytes and megabytes, and now they are talking only about gigabytes and terabytes. As soon as programmers offer a more or less satisfactory solution of the proposed problems, new, even more complex problems immediately arise. Programmers invent new methods, create new languages. For half a century, several hundred languages have appeared, many methods and styles have been proposed. Some methods and styles become generally accepted and form for some time the so-called programming paradigm.
Programming paradigms
The first, even the simplest programs written in machine codes, made up hundreds of lines of completely incomprehensible text. To simplify and speed up programming, they invented high-level languages: FORTRAN, Algol and hundreds of others, assigning routine operations to create machine code to the compiler. The same programs, rewritten in high-level languages, have become much clearer and shorter. But life demanded the solution of more complex problems, and the programs again increased in size, became immense.
An idea arose: to arrange a program in the form of several, as simple as possible, procedures or functions, each of which solves its specific task. Write, compile and debug a small procedure can be easily and quickly. Then it remains only to collect all the procedures in the correct order in one program. In addition, once written procedures can then be used in other programs as building blocks. Procedural programming quickly became a paradigm. All high-level languages have included tools for writing procedures and functions. There were many libraries of procedures and functions for all occasions.
The question arose of how to identify the structure of the program, split the program into procedures, what part of the code to separate into a separate procedure, how to make the problem solving algorithm simple and intuitive, how convenient it is to link the procedures together. Experienced programmers offered their recommendations, called structured programming. Structural programming has proven convenient and has become a paradigm. There are programming languages, such as Pascal, in which it is convenient to write structural programs. Moreover, it is very difficult to write non-structural programs on them.
The complexity of the tasks faced by programmers was manifested here as well: the program began to contain hundreds of procedures, and again proved to be immense. The bricks have become too small. It took a new programming style,
At the same time, it was found that a successful or unsuccessful source data structure can greatly facilitate or complicate their processing. Some source data is more convenient to combine into an array, for others the structure of a tree or stack is more suitable. Niklaus Wirth even called his book Algorithms + Data Structures = Programs.
There was an idea to combine the source data and all the procedures for their processing into a single module. This idea of modular programming quickly won minds and for some time became a paradigm. The programs were composed of separate modules containing a dozen other procedures and functions. The effectiveness of such programs is higher, the smaller the modules depend on each other. The autonomy of the modules allows you to create and module libraries, then to use them as building blocks for the program.
In order to ensure maximum independence of the modules from each other, it is necessary to clearly separate the procedures that will be called by other modules - public (public) procedures, from auxiliary ones that process the data contained in this module, - private (private) procedures. The first are listed in a separate part of the module - the interface (interface), the second are only involved in the implementation of the module. The data entered in the module is also divided into open, specified in the interface and available for other modules, and closed, available only for procedures of the same module. In different programming languages, this division is done in different ways. In the Turbo Pascal language, the module is specifically divided into an interface and the implementation of the language C interface is made into separate "header" files. In the language of C ++, in addition, to describe the interface, you can use abstract classes. In Java, there is a special construct for describing interfaces, which is called interface, but you can also write abstract classes.
So the idea of hiding, encapsulating (incapsulation) data and methods for their processing. Similar ideas periodically arise in the design of household appliances. That TVs are speckled with buttons and puffed up with handles and engines to the joy of an inquisitive viewer, the “instrument” style dominates, then suddenly everything disappears somewhere, and only the power button and the volume knob remain on the panel. Inquisitive viewer is taken for a screwdriver.
Encapsulation, of course, is not made to hide something curious from another module. There are two main goals here. The first is to ensure the security of the use of the module, to bring to the interface, to make publicly available only those information processing methods that cannot spoil or delete the original data. The second goal is to reduce the complexity by hiding unnecessary implementation details from the outside world.
Again, the question arose how to break the program into modules? Here the methods of solving the old programming problem turned out to be the way - modeling the actions of artificial and natural objects: robots, machine tools with programmed control, unmanned aircraft, people, animals, plants, life support systems, process control systems.
In fact, every object - a robot, a car, a man - has certain characteristics. They can serve: weight, height, maximum speed, angle of rotation, load capacity, surname, age. The object can perform some actions: move in space, rotate, lift, dig, grow or shrink, eat, drink, be born and die, changing its original characteristics. It is convenient to simulate an object as a module. Its characteristics will be data, constant or variable, and actions will be procedures.
It turned out to be convenient to do the opposite - to break the program into modules so that it becomes a set of interacting objects. Thus, object-oriented programming, or OOP, is a modern programming paradigm.
In the form of objects, you can imagine quite unexpected concepts. For example, a window on the display screen is an object that has a width and height , the location on the screen, usually described by the (x, y) coordinates of the upper left corner of the window, and the font that displays the text in the window, say, Times New Roman, background color , a few buttons, scroll bars and other features. The window can be moved around the screen using the move () method, enlarged or reduced in size using the size () method, minimized to a shortcut using the iconify () method, and somehow respond to mouse actions and keystrokes. This is a complete object! Buttons, scroll bars, and other window elements are also objects with their own sizes, fonts, and movements.
Of course, to assume that the window itself “knows how” to perform actions, and we only give it instructions: “Turn, turn, move” - this is a somewhat unexpected look at things, but now you can give commands not only with the mouse and keys, but also by voice!
The idea of object-oriented programming proved to be very fruitful and began to actively develop. It turned out that it is convenient to set the task immediately in the form of a set of active objects - an object-oriented analysis, OOA , emerged. We decided to design complex systems in the form of objects — object-oriented design, OOP (OOD, object-oriented design) appeared.
Let's take a closer look at the principles of object-oriented programming.
Principles of object-oriented programming
Object-oriented programming has been evolving for over twenty years. There are several schools, each of which offers its own set of principles for working with objects and sets out these principles in its own way. But there are several generally accepted concepts. We list them.
Abstraction
Describing the behavior of an object, such as a car, we build its model. The model, as a rule, cannot describe the object completely, the real objects are too complex. We have to select only those characteristics of the object that are important for solving the task set before us. For the description of cargo transportation, an important characteristic will be the load carrying capacity of the car, and for the description of automobile racing it is not essential. But in order to simulate races, it is necessary to describe the method for speeding up this vehicle, and for cargo transportation this is not so important.
We must abstract away from some specific details of the object. It is very important to choose the right degree of abstraction. Too high a degree will give only an approximate description of the object, will not allow to model its behavior correctly. A too low degree of abstraction will make the model very complex, overloaded with details, and therefore unsuitable.
For example, you can absolutely accurately predict the weather for tomorrow at a certain place, but calculations based on this model will last for three days even on the most powerful computer. Why do we need a model that is two days late? But the accuracy of the model used by forecasters, we all know ourselves. But the calculations for this model take only a few hours.
The description of each model is made in the form of one or several classes (classes). A class can be considered a project, an impression, a drawing on which specific objects will then be created. The class contains the description of variables and constants characterizing the object. They are called class fields. Procedures that describe the behavior of an object are called class methods. Inside a class, you can describe both nested classes and nested interfaces. Fields, methods and nested classes of the first level are class members. Different schools of object-oriented programming offer different terms, we use the terminology adopted in Java technology.
Here is a sketch of the description of the car:
class Automobile {
int maxVelocity; // The field containing the highest vehicle speed
int speed; // Field containing the current vehicle speed
int weight; // Field containing vehicle weight
// Other fields ...
void moveTo (int x, int y) {// Method that simulates a move
// car. Parameters x and y are not fields
int a = 1; // Local variable is not a field
// Method body. It describes the law
// move the car to a point (x, y)
}
// Other methods. . .
}
Connoisseurs of Pascal
There are no nested procedures and functions in Java, you cannot describe another method in the method body.
After the class description is complete, you can create specific objects, instances of the described class. Creating instances is done in three steps, like the description of arrays. First, object references are declared: the name of the class is written, and the space is used to list instances of the class, or more precisely, references to them.
Automobile Iada2110, fordScorpio, oka;
Then the new operation defines the objects themselves, allocates memory for them, the link gets the address of this area as its value.
Iada2110 = new Automobile ();
fordScorpio = new Automobile ();
oka = new Automobile (};
At the third stage, objects are initialized, initial values are set. This stage, as a rule, is combined with the second one; it is for this purpose that the new operation repeats the name of the class with the brackets Automobile () . This is the so-called constructor of a class, but we'll talk about it later.
Since the names of fields, methods, and nested classes are the same for all objects, they are specified in the class description, they need to be specified with the name of the object reference:
lada2110.maxVelocity = 150;
fordScorpio.maxVelocity = 180;
oka.maxVelocity = 350; // Why not?
oka.moveTo (35, 120);
Recall that a quoted text string is understood in Java as an object of the String class. Therefore, you can write
int strlen = "This is an object of class String" .length ();
The string object executes the length () method, one of the methods in its class string , counting the number of characters in a string. As a result, we get the value of strlen , equal to 24. Such a strange entry is found in Java programs at every step.
In many situations, build several models with varying degrees of detail. For example, to construct a coat and a fur coat, a less accurate model of the contours of the human body and its movements is needed, and to construct a dress coat or evening dress, it is already much more accurate. In this case, a more accurate model, with a lesser degree of abstraction, will use the already existing methods of a less accurate model.
Do you think that the class Automobile is heavily overloaded? Indeed, millions of cars of different brands and types are produced in the world. What is common between them, except for the four wheels? Yes, and the wheels may be larger or smaller. Wouldn't it be better to write separate classes for cars and trucks, for racing cars and all-terrain vehicles? How to organize all this many classes? Object-oriented programming answers this question as follows: it is necessary to organize a hierarchy of classes.
Hierarchy
The hierarchy of objects has long been used to classify them. It is especially detailed in biology. Everyone is familiar with families, genera and species. We can make a description of our pets: cats, dogs, cows and others as follows:
class Pet {// Here we describe the general properties of all pets
Master person; // The owner of the animal
int weight, age, eatTimel]; // Weight, age, feeding time
int eat (int food, int drink, int time) {// feeding process
// Initial actions ...
if (time == eatTimefi]) person.getFood (food, drink);
// Method of food consumption
}
void voice (); // Animal Sounds
// Other ...
}
Then we create classes that describe more specific objects by associating them with a general class:
class Cat extends Pet {// Describes properties that are unique to cats:
int mouseCatched; // number of mice caught
void toMouse (); // process of catching mice
// Other properties
}
class Dog extends Pet {// Properties of dogs:
void preserve (); // guard
}
Notice that we do not repeat the common properties described in the Pet class. They are inherited automatically. We can define an object of the Dog class and use all the properties of the Pet class in it as if they were described in the Dog class:
Dog tuzik = new Dog (), sharik = new Dog ();
After this definition, you can write
tuzik.age = 3;
int p = sharik.eat (30, 10, 12);
And continue the classification as follows:
class Pointer extends Dog {...} // Pointer Properties
class Setter extends Dog {...} // Setters properties
Notice that at every next level of the hierarchy new properties are added to the class, but none of the properties disappear. Therefore, the word extends is used - "expands" and they say that the class Dog is an extension of the class Pet . On the other hand, the number of objects decreases: there are fewer dogs than all pets. Therefore, it is often said that the Dog class is a subclass of the Pet class, and the Pet class is the superclass or the superclass of the Dog class.
Genealogical terminology is often used: the parent class, the child class, the descendant class, the ancestor class, nephews and grandchildren arise, the entire restless family enters into a relationship worthy of a Mexican TV series.
In this terminology, they talk about inheritance of classes, in our example, the class Dog inherits the class Pet .
We have not yet identified the happy owner of our home zoo. We describe it in the class Master. We make a sketch:
class Master {// Master of the animal
String name; // Last Name, First Name
// Other information
void getFood (int food, int drink); // Feeding
// Other
}
The owner and his pets are constantly in contact in life. Their interaction is expressed by the verbs "walk", "feed", "guard", "clean", "caress", "ask" and others. To describe the interaction of objects, the third principle of object-oriented programming is applied - duty or responsibility.
A responsibility
In our example, only the interaction in the feeding process described by the eat () method is considered. In this method, the animal refers to the owner, begging him to use the getFood () method.
In the English literature, this treatment is described by the word message. This concept is unsuccessfully translated into Russian by the non-binding word "message". It would be better to use the word "message", "order" or even "disposal." But the term "message" is settled and we will have to use it. Why is not used the phrase "method call", because they say: "Procedure call"? Because between these concepts there are at least three differences.
Итак, объект sharik , выполняя свой метод eat () , посылает сообщение объекту, ссылка на который содержится в переменной person, с просьбой выдать ему определенное количество еды и питья. Сообщение записано в строке person.getFood(food, drink) .
Этим сообщением заключается контракт (contract) между объектами, суть которого в том, что объект sharik берет на себя ответственность (responsibility) задать правильные параметры в сообщении, а объект — текущее значение person — возлагает на себя ответственность применить метод кормления getFood() , каким бы он ни был.
Для того чтобы правильно реализовать принцип ответственности, применяется четвертый принцип объектно-ориентированного программирования — модульность (modularity).
Модульность
Этот принцип утверждает — каждый класс должен составлять отдельный модуль. Члены класса, к которым не планируется обращение извне, должны быть инкапсулированы.
In Java, encapsulation is achieved by adding a private modifier to the description of a class member. For example:
private int mouseCatched;
private String name;
private void preserve ();
These class members become private, they can only be used by instances of the same class, for example, tuzik can give instructions
sharik.preserve ().
And if in the Master class we write
private void getFood (int food, int drink);
then the getFood () method will not be found, and the unfortunate sharik will not be able to get food. ,
In contrast to being closed, we can declare some class members open by writing the public modifier instead of the word private , for example:
public void getFood(int food, int drink);
К таким членам может обратиться любой объект любого класса.
Знатокам C++
В языке Java словами private, public и protected отмечается каждый член класса в отдельности.
Принцип модульности предписывает открывать члены класса только в случае необходимости. Вспомните надпись: "Нормальное положение шлагбаума — закрытое".
Если же надо обратиться к полю класса, то рекомендуется включить в класс специальные методы доступа (access methods), отдельно для чтения этого поля (get method) и для записи в это поле (set method). Имена методов доступа рекомендуется начинать со слов get и set , добавляя к этим словам имя поля. Для JavaBeans эти рекомендации возведены в ранг закона.
In our example of the Master class, the methods for accessing the Name field in their simplest form can look like this:
public String getName () {
return name;
}
public void setName (String newName)
{
name = newName;
}
In real situations, access is limited by various checks, especially in set-methods that change field values. You can check the type of input value, set the range of values, compare with the list of valid values.
In addition to access methods, it is recommended to create verification is-methods that return the boolean value true or false . For example, you can include a method in the Master class that checks whether the host name is set:
public boolean isEmpty () {
return name == null? true: false;
}
и использовать этот метод для проверки при доступе к полю Name , например:
if (masterOl.isEmpty()) masterOl.setName("Иванов");
Итак, мы оставляем открытыми только методы, необходимые для взаимодействия объектов. При этом удобно спланировать классы так, чтобы зависимость между ними была наименьшей, как принято говорить в теории ООП, было наименьшее зацепление (low coupling) между классами. Тогда структура программы сильно упрощается. Кроме того, такие классы удобно использовать как строительные блоки для построения других программ.
Напротив, члены класса должны активно взаимодействовать друг с другом, как говорят, иметь тесную функциональную связность (high cohestion). Для этого в класс следует включать все методы, описывающие поведение моделируемого объекта, и только такие методы, ничего лишнего. Одно из правил достижения сильной функциональной связности, введенное Карлом Ли-берхером (Karl J. Lieberherr), получило название закон Деметра. Закон гласит: "в методе т() класса А следует использовать только методы класса А, методы классов, к которым принадлежат аргументы метода т(), и методы классов, экземпляры которых создаются внутри метода m ().
Объекты, построенные по этим правилам, подобны кораблям, снабженным всем необходимым. Они уходят в автономное плавание, готовые выполнить любое поручение, на которое рассчитана их конструкция.
Будут ли закрытые члены класса доступны его наследникам? Если в классе Pet написано
private Master person;
то можно ли использовать sharik.person ? Разумеется, нет. Ведь в противном случае каждый, интересующийся закрытыми полями класса А , может расширить его классом B , и просмотреть закрытые поля класса А через экземпляры класса B .
Когда надо разрешить доступ наследникам класса, но нежелательно открывать его всему миру, тогда в Java используется защищенный (protected) доступ, отмечаемый модификатором protected , например, объект sharik может обратиться к полю person родительского класса pet , если в классе Pet это поле описано так:
protected Master person;
Следует сразу сказать, что на доступ к члену класса влияет еще и пакет, в котором находится класс, но об этом поговорим в следующей главе.
Из этого общего схематического описания принципов объектно-ориентированного программирования видно, что язык Java позволяет легко воплощать все эти принципы. Вы уже поняли, как записать класс, его поля и методы, как инкапсулировать члены класса, как сделать расширение класса и какими принципами следует при этом пользоваться. Разберем теперь подробнее правила записи классов и рассмотрим дополнительные их возможности.
Но, говоря о принципах ООП, я не могу удержаться от того, чтобы не напомнить основной принцип всякого программирования.
Принцип KISS
Самый основной, базовый и самый великий : принцип программирования — принцип KISS — не нуждается в разъяснений : и переводе: "Keep It Simple, Stupid!"
Как описать класс и подкласс
Итак, описание класса начинается со слова class, после которого записывается имя класса. Соглашения "Code Conventions" рекомендуют начинать имя класса с заглавной буквы.
Перед словом class можно записать модификаторы класса (class modifiers). Это одно из слов public, abstract, final, strictfp . Перед именем вложенного класса можно поставить, кроме того, модификаторы protected, private, static . Модификаторы мы будем вводить по мере изучения языка.
Тело класса, в котором в любом порядке перечисляются поля, методы, вложенные классы и интерфейсы, заключается в фигурные скобки.
При описании поля указывается его тип, затем, через пробел, имя и, может быть, начальное значение после знака равенства, которое можно записать константным выражением. Все это уже описано в главе 1.
Описание поля может начинаться с одного или нескольких необязательных модификаторов public, protected, private, static, final, transient, volatile . Если надо поставить несколько модификаторов, то перечислять их JLS рекомендует в указанном порядке, поскольку некоторые компиляторы требуют определенного порядка записи модификаторов. С модификаторами мы будем знакомиться по мере необходимости.
When describing a method, the type of value returned by it or the word void is indicated, then, separated by a space, the name of the method, then, in brackets, the list of parameters. After that in curly brackets the executed method signs.
The method description can begin with the modifiers public, protected, private, abstract, static, final, synchronized, native, strictfp . We will introduce them as needed.
A comma-separated list of parameters lists the type and name of each parameter. Before the type of any parameter may be a final modifier. This parameter cannot be changed inside the method. The parameter list may be missing, but the brackets are preserved.
Before the method starts, for each parameter, a memory cell is allocated to which the parameter value specified during the method call is copied. This method is called passing parameters by value.
Listing 2.1 shows how to divide the method of dividing in half to find the root of the non-linear equation from Listing 1.5.
Listing 2.1. Finding the root of a nonlinear equation by the bisection method
class Bisection2 {
private static double final EPS = le-8; // Constant
private double a = 0.0, b = 1.5, root; // Closed Fields
public double getRoot (} {return root;} // Access Method
private double f (double x)
{
return x * x * x - 3 * x * x + 3; // Or something different
}
private void bisect () {// No parameters -
// method works with instance fields
double y = 0.0; // Local variable is not a field
do {
root = 0.5 * (a + b); y = f (root);
if (Math.abs (y) <EPS) break;
// The root is found. We leave cycle
// If at the ends of the segment [a; root]
// function has different signs:
if (f (a) * y <0.0} b = root;
// means root is here
// Move point b to root
//Otherwise:
else a = root;
// move point a to root
// Continue until [a; B] will not be small
} while (Math.abs (ba)> = EPS);
}
public static void main (String [] args) {
Bisection2 b2 = new Bisection2 ();
b2.bisect ();
System.out.println ("x =" +
b2.getRoot () + // Access the root through the access method
", f () =" + b2.f (b2.getRoot ()));
}
}
In the description of the f () method, the old procedural style is preserved: the method takes an argument, processes it and returns the result. The description of the bisect method is done in the spirit of OOP: the method is active, it itself refers to the fields of instance b2 and puts the result in the required field itself. The bisect () method is an internal mechanism of the Bisection2 class, so it is private.
The name of the method, the number and types of the parameters form the signature (signature) of the method. The compiler distinguishes methods not by their names, but by signatures. This allows you to record different methods with the same name, differing in the number and / or types of parameters.
Comment
The type of the return value is not included in the method signature, which means that the methods cannot differ only in the type of the result of their work.
For example, in the Automobile class, we recorded the moveTo method (int x, int y) , denoting the destination by its geographical coordinates. You can also define the moveTo (string destination) method to specify the geographical name of the destination and refer to it like this:
oka.moveTo ("Moscow");
This duplication of methods is called overloading . Method overloading is very convenient to use. Recall that in Chapter 1 we output data of any type to the screen using the printin () method without worrying about which data type we output. In fact, we used different methods t with the same name printin , without even thinking about it. Of course, all these methods must be carefully planned and described in advance in class. This is done in the Printstream class, where about twenty print () and println () methods are represented.
If you write a method with the same name in a subclass, for example:
class Truck extends Automobile {
void moveTo (int x, int y) {
// Any action
}
// Something else
}
then it will override the superclass method. Defining an instance of the class Truck , for example:
Truck gazel = new Truck ();
and writing gazei.moveTo (25, 150) , we turn to the class method Truck . This will override the method.
At redefinition of access rights to a method it is possible to expand only. The public method public must remain open, protected protected may become open.
Is it possible to refer to a superclass method inside a subclass? Yes, you can, if you specify the name of the method, with the word super , for example, super.moveTo (30, 40) . You can also specify the name of the method written in the same class with the word this , for example, this.moveTo (50, 70) , but in this case it is already unnecessary. In the same way, you can specify the matching field names, not just methods.
These refinements are similar to how we say “I” to ourselves, not “Ivan Petrovich”, and say “father”, and not “Peter Sidorovich”.
Redefinition of methods leads to interesting results. In the Pet class, we described the voice () method. Redefine it in subclasses and use it in the chorus class, as shown in listing 2.2.
Listing 2.2. Polymorphic example
abstract class Pet {
abstract void voice ();
}
class Dog extends Pet {
int k = 10;
void voice () {
System.out.printin ("Gav-gav!");
}
}
class Cat extends Pet {
void voice () {
System.out.printin ("Miaou!");
}
}
class Cow extends Pet {
void voice () {
System.out.printin ("Mu-uu!");
}
}
public class Chorus (
public static void main (String [] args) {
Pet [] singer = new Pet [3];
singer [0] = new Dog ();
singer [1] = new Cat ();
singer [2] = new Cow ();
for (int i = 0; i <singer.length; i ++)
singer [i] .voice ();
}
}
In fig. 2.1 shows the output of this program. Animals sing their voices!
It's all about the singer [] field definition . Although the singer [] array of links is of type Pet , each element of it refers to an object of its own type Dog, Cat, cow . When the program is executed, the method of the specific object is called, and not the class method that determined the name of the link. This is how polymorphism is implemented in Java.
Connoisseurs of C ++
In Java, all methods are virtual functions.
The attentive reader noted in the description of the Pet class a new word abstract . The Pet class and the voice () method are abstract.
Fig. 2.1. Chorus Program Result
Abstract methods and classes
When describing the Pet class, we cannot specify any useful algorithm in the voice () method, since all animals have completely different voices.
In such cases, we write only the method header and put a semicolon after the closing parameter list. This method will be abstract (abstract), which must be specified by the modifier as an abstract modifier.
If a class contains at least one abstract method, then it will not be possible to create its instances, let alone use them. Such a class becomes abstract, which must be specified with the abstract modifier.
How to use abstract classes? Only generating from them subclasses in which abstract methods are redefined.
Why do we need abstract classes? Wouldn’t it be better to write the right classes with fully defined methods instead of inheriting them from an abstract class? To answer again, we turn to listing 2.2.
Although the elements of the singer [] array refer to the Dog, Cat, Cow subclasses, they are still variables of type Pet and they can only refer to the fields and methods described in the Pet superclass. Additional subclass fields are not available for them. Try to refer, for example, to the k field of the Dog class by writing singer [0] .k . The compiler will "say" that it cannot implement such a link. Therefore, a method that is implemented in several subclasses has to be put into the superclass, and if it cannot be implemented there, then declare it abstract. Thus, abstract classes are grouped at the top of the class hierarchy.
By the way, you can set an empty implementation of the method by simply putting a pair of curly brackets, without writing anything between them, for example:
void voice () {}
Get a complete method. But this is an artificial decision that confuses the class structure.
You can close the hierarchy with final classes.
Final members and classes
By marking a method as a final modifier, you can prevent it from being redefined in subclasses. This is convenient for security purposes. You can be sure that the method performs the actions that you specified. This is how the mathematical functions sin (), cos () and others are defined in the class Math . We are sure that the Math.cos (x) method calculates the cosine of the number x . Of course, such a method cannot be abstract.
For complete security, the fields processed by final methods should be made private.
If we mark the whole class with the final modifier, then it cannot be expanded at all. So, for example, the class Math is defined:
public final class Math {. . . }
For variables, the final modifier has a completely different meaning. If you mark the variable description with the final modifier, then its value (and it must be specified either here, in the initialization block or in the constructor) cannot be changed either in subclasses or in the class itself. The variable turns into a constant. This is how constants are defined in the Java language:
public final int MIN_VALUE = -1, MAX_VALUE = 9999;
By convention "Code Conventions" constants are written in capital letters, the words in them are separated by an underscore.
At the very top of the Java class hierarchy is the Object class.
Class object
If we do not specify any extension when describing a class, that is, we do not write the word extends and the class name behind it, as in the description of the Pet class, then Java considers this class an extension of the object class, and the compiler appends it for us:
class Pet extends Object {. . . }
You can write this extension and clearly.
The object class itself is not anybody's successor; the hierarchy of any Java classes begins from it. In particular, all arrays are direct heirs of the object class.
Since such a class can contain only general properties of all classes, it includes only a few of the most common methods, for example, the equals () method, which compares this object for equality with the object specified in the argument, and returns a boolean value. You can use it like this:
Object objl = new Dog (), obj 2 = new Cat ();
if (obj1.equals (obj2)) ...
Rate the object-oriented spirit of this record: the object obj1 is active, it compares itself with another object. You can, of course, write obj2.equals (obj1) , by making the object obj2 active, with the same result.
As mentioned in Chapter 1, references can be compared to equality and inequality:
obj1 == obj2; obj1! = obj 2;
In this case, the addresses of the objects are matched; we can find out whether both references to the same object indicate.
The equals () method compares the contents of objects in their current state; in fact, it is implemented in the object class as an identity: an object is equal only to itself. Therefore, it is often redefined in subclasses; moreover, properly designed, well-educated classes should redefine the methods of the object class if they are not satisfied with the standard implementation.
The second method of the object class, which should be overridden in subclasses, is the tostring () method. It is a method with no parameters that tries to convert the contents of an object to a character string and returns an object of class string .
The Java runtime system accesses this method each time an object is required to be represented as a string, for example, in the printing method.
Class constructors
You have already noticed that the new operation, which defines instances of the class, repeats the name of the class with brackets. This is similar to a method call, but what is a “method” whose name completely coincides with the class name?
Such a "method" is called a class constructor. His character is not only a name. We list the features of the constructor.
If super () is not specified at the beginning of the constructor, the constructor of the superclass is first executed with no arguments, then the fields are initialized with the values specified when they are declared, and then what is written in the constructor.
In all other respects, the constructor can be considered the usual method, it is allowed to write any statements, even the return statement , but only empty, without any return value.
In a class there can be several constructors. Since they have the same name, which coincides with the name of the class, they must differ in the type and / or number of parameters.
In our examples, we have never considered the class constructors, so when creating instances of our classes, the object class constructor was called.
New operation
It is time to describe in more detail the operation with one operand, denoted by the word new . It is used to allocate memory to arrays and objects.
In the first case, the operand is the type of the array elements and the number of its elements in square brackets, for example:
double a [] = new double [100];
In the second case, the operand is the class constructor. If there is no constructor in the class, the default constructor is called.
Class numeric fields get null values, boolean fields get false , references null .
The result of the new operation will be a link to the created object. This reference can be assigned to a variable of type reference to this type:
Dog k9 = new Dog ();
but can be used directly
new Dog (). voice ();
Here, after creating an unnamed object, its voice () method is immediately executed. Such a strange record is found in programs written in Java, at every step.
Static class members
Different instances of the same class have completely independent fields; they take different values. Changing a field in one instance does not affect the same field in another instance. Each instance is allocated its own memory cell for such fields. Therefore, these fields are called instance variables of the class (instance variables) or object variables .
Sometimes it is necessary to define a field that is common to the entire class, and changing it in one instance will change the same field in all instances. For example, we want to mark the serial serial number of the car in the Automobile class. Such fields are called class variables. For class variables, only one memory cell is allocated, common to all instances. Class variables are generated in Java with the static modifier. In Listing 2.3, we write this modifier when defining the variable number .
Listing 2.3. Static variable
class Automobile {
private static int number;
Automobile () {
number ++;
System.out.println ("From Automobile constructor:" +
"number =" + number);
}
}
public class AutomobiieTest {
public static void main (String [] args) {
Automobile lada2105 = new Automobile (),
fordScorpio = new Automobile (),
oka = new Automobile!);
}
}
We get the result shown in fig. 2.2.
Fig. 2.2. Static variable change
Interestingly, static variables can be accessed with the class name, Automobile.number , and not just with the instance name, lada2105.number , and this can be done even if no instance of the class has been created.
To work with such static variables , static methods are usually created that are marked with the static modifier. For methods, the word static has a completely different meaning. The Java runtime system always creates in memory only one copy of the machine code of a method, shared by all instances, whether static is a method or not.
The main feature of static methods is that they are executed at once in all instances of the class. Moreover, they can be executed even if no instance of the class has been created. It is enough to clarify the name of the method with the name of the class (and not the name of the object) for the method to work. This is how we used the methods of the Math class, not creating instances of it, but simply writing Math.abs (x), Math.sqrt (x ). Similarly, we used the System, out method . println () . And we use the main () method, without creating any objects at all.
Therefore, static methods are called class methods (class methods), in contrast to non-static methods, called instance methods.
This leads to other features of static methods:
That is why in Listing 1.5 we marked the f () method with the static modifier. But in Listing 2.1, we worked with the b2 instance of the Bisection2 class, and we did not need to declare the f () method static.
Static variables are initialized before the constructor starts working, but only initial expressions can be used during initialization. If initialization requires complex calculations, for example, cycles to assign values to the elements of static arrays or calls to methods, then these calculations are enclosed in a block marked with the word static , which will also be executed before running the constructor:
static int [] a = new a [10];
static {
for (int k = 0; k <a.length; k ++)
a [k] = k * k;
}
Operators enclosed in such a block are executed only once, when the class is first loaded, and not at the creation of each instance.
Here the attentive reader probably caught me: "But he said that all actions are performed only with the help of methods!" I repent: static initialization blocks, and instance initialization blocks are written outside of any methods and executed before starting to execute not only a method, but even a constructor.
Complex class
Complex numbers are widely used not only in mathematics. They are often used in graphic transformations, in the construction of fractals, not to mention physics and technical disciplines. But the class describing complex numbers is for some reason not included in the standard Java library. Восполним этот пробел.
Listing 2.4 is long, but look at it carefully; reading a program in that language is very useful when learning a programming language. Moreover, only programs are worth reading, the author’s explanations only prevent one from understanding the meaning of the actions (joke).
Listing 2.4. Complex class
class Complex {
private static final double EPS = le-12; // Calculation Accuracy
private double re, im; // Real and imaginary part
// Four constructors
Complex (double re, double im) {
this, re = re; this.im = im;
}
Complex (double re) {this (re, 0.0); }
Complex () {this (0.0, 0.0); }
Complex (Complex z) {this (z.re, z.im); }
// Access methods
public double getRe () {return re;}
public double getlmf) {return im;}
public Complex getZ () {return new Complex (re, im);}
public void setRe (double re) {this.re = re;}
public void setlm (double im) {this.im = im;}
public void setZ (Complex z) {re = z.re; im = z.im;}
// Module and argument of a complex number
public double mod () {return Math.sqrt (re * re + im * im);}
public double arg () (return Math.atan2 (re, im);}
// Check: real number?
public boolean isReal () {return Math.abs (im) <EPS;}
public void pr () {// Display
System.out.println (re + (im <0.0? "": '"+") + Im + "i");
}
// Override the methods of the Object class
public boolean equals (Complex z) {
return Math.abs (re -'z.re) <EPS &&
Math.abs (im - z.im) <eps;
}
public String toString () {
return "Complex:" + re + "" + im;
}
// Methods that implement the operations + =, - =, * =, / =
public void add (Complex z) {re + = z.re; im + = z.im;}
public void sub (Complex z) {re - = z.re; im - = z.im;}
public void mul (Complex z) {
double t = re * z.re - im * z. im;
im = re * z.im + im * z.re;
re = t;
}
public void div(Complex z){
double m = z.mod();
double t = re * z.re — im * z.im;
im = (im * z.re — re * z.im) / m;
re = t / m;
}
// Методы, реализующие операции +, -, *, /
public Complex plus(Complex z){
return new Complex(re + z.re, im + z im);
}
public Complex minus(Complex z){
return new Complex(re - z.re, im - z.im);
}
public Complex asterisk(Complex z){
return new Complex(
re * z.re - im * z.im, re * z.im + im * z re);
}
public Complex slash(Complex z){
double m = z.mod();
return new Complex(
(re * z.re - im * z.im) / m, (im * z.re - re * z.im) / m);
}
}
// Проверим работу класса Complex
public class ComplexTest{
public static void main(Stringf] args){
Complex zl = new Complex(),
z2 = new Complex(1.5),
z3 = new Complex(3.6, -2.2),
z4 = new Complex(z3);
System.out.printlnf); // Оставляем пустую строку
System.out.print("zl = "); zl.pr();
System.out.print("z2 = "); z2.pr();
System.out.print("z3 = "); z3.pr();
System.out.print ("z4 = "}; z4.pr();
System.out.println(z4); // Работает метод toString()
z2.add(z3);
System.out.print("z2 + z3 = "}; z2.pr();
z2.div(z3);
System.out.print("z2 / z3 = "); z2.pr();
z2 = z2.plus(z2);
System.out.print("z2 + z2 = "); z2.pr();
z3 = z2.slash(zl);
System.out.print("z2 / zl = "); z3.pr();
}
}
In fig. 2.3 показан вывод этой программы.
Fig. 2.3. Вывод программы ComplexTest
Метод main()
Всякая программа, оформленная как приложение (application), должна содержать метод с именем main . Он может быть один на все приложение или содержаться в некоторых классах этого приложения, а может находиться и в каждом классе.
Метод main() записывается как обычный метод, может содержать любые описания и действия, но он обязательно должен быть открытым ( public ), статическим ( static ), не иметь возвращаемого значения ( void ). Его аргументом обязательно должен быть массив строк ( string[] ). По традиции этот массив называют args , хотя имя может быть любым.
Эти особенности возникают из-за того, что метод main() вызывается автоматически исполняющей системой Java в самом начале выполнения приложения. При вызове интерпретатора java указывается класс, где записан метод main() , с которого надо начать выполнение. Поскольку классов с методом main() может быть несколько, можно построить приложение с дополнительными точками входа, начиная выполнение приложения в разных ситуациях из различных классов.
Часто метод main() заносят в каждый класс с целью отладки. В этом случае в метод main() включают тесты для проверки работы всех методов класса.
При вызове интерпретатора java можно передать в метод main() несколько параметров, которые интерпретатор заносит в массив строк. Эти параметры перечисляются в строке вызова java через пробел сразу после имени класса. Если же параметр содержит пробелы, надо заключить его в кавычки. Кавычки не будут включены в параметр, это только ограничители.
Все это легко понять на примере листинга 2.5, в котором записана программа, просто выводящая параметры, передаваемые в метод main() при запуске.
Листинг 2.5. Передача параметров в метод main()
class Echo {
public static void main (String [] args) {
for (int i = 0; i < args.length; i++)
System.out.println("args[" + i +"]="+ args[i]);
}
}
In fig. 2.4 показаны результаты работы этой программы с разными вариантами задания параметров.
Fig. 2.4. Вывод параметров командной строки
Как видите, имя класса не входит в число параметров. Оно и так известно в методе main() .
Знатокам C/C++
Поскольку в Java имя файла всегда совпадает с именем класса, содержащего метод main() , оно не заносится в args[0] . Вместо argc используется args. length. Доступ к переменным среды разрешен не всегда и осуществляется другим способом. Некоторые значения можно просмотреть так: System.getProperties().list(System.out);.
Где видны переменные
В языке Java нестатические переменные можно объявлять в любом месте кода между операторами. Статические переменные могут быть только полями класса, а значит, не могут объявляться внутри методов и блоков. Какова же область видимости (scope) переменных? Из каких методов мы можем обратиться к той или иной переменной? В каких операторах использовать? Рассмотрим на примере листинга 2.6 разные случаи объявления переменных.
Листинг 2.6. Видимость и инициализация переменных
class ManyVariables{
static int x = 9, у; // Статические переменные — поля класса
// Они известны во всех методах и блоках класса
// Переменная у получает значение 0
static{ // Блок инициализации статических переменных
// Выполняется один раз при первой загрузке класса после
// инициализаций в объявлениях переменных
x = 99; // Operator is executed outside of any method!
}
int a = 1, p; // Non-static variables - instance fields
// Known in all methods and blocks of the class in which they
// not covered by other variables with the same name
// Variable p gets the value 0
{// instance initialization block
// Executed at creation, each instance after
// initializations for variable declarations
p = 999; // Operator is executed outside of any method!
}
static void f (int b) {// Method parameter b - local
// variable, known only inside the method
int a = 2; // This is the second variable with the same name "a"
// It is known only inside the f () method and
// overlaps the first "a" here
int with; // Local variable, known only in the f () method
// gets no initial value
// and must be defined before use
{int c = 555; // Mistake! Attempt to re-announce
int x = 333; // Local variable, known only in this block
}
// Here, the variable x is already unknown
for (int d = 0; d <10; d ++) {
// The loop variable d is known only in the loop
int a = 4; // Mistake!
int e = 5; // Local variable, known only in the for loop
e ++; // Initialized every time the loop runs
System.out.println ("e =" + e); // Output is always "e = 6"
}
// Here the variables d and e are unknown
}
public static void main (String!] args) {
int a = 9999; // Local variable known
// only inside the main () method
f (a);
}
}
Обратите внимание на то, что переменным класса и экземпляра неявно присваиваются нулевые значения. Символы неявно получают значение '\u0000' , логические переменные — значение false , ссылки получают неявно значение null .
Локальные же переменные неявно не инициализируются. Им должны либо явно присваиваться значения, либо они обязаны определяться до первого использования. К счастью, компилятор замечает неопределенные локальные переменные и сообщает о них.
Внимание
Поля класса при объявлении обнуляются, локальные переменные автоматически не инициализируются.
В листинге 2.6 появилась еще одна новая конструкция: блок инициализации экземпляра (instance initialization). Это просто блок операторов в фигурных скобках, но записывается он вне всякого метода, прямо в теле класса. Этот блок выполняется при создании каждого экземпляра, после инициализации при объявлении переменных, но до выполнения конструктора. Он играет такую же роль, как и static-блок для статических переменных. Зачем же он нужен, ведь все его содержимое можно написать в начале конструктора? В тех случаях, когда конструктор написать нельзя, а именно, в безымянных внутренних классах.
Вложенные классы
В этой главе уже несколько раз упоминалось, что в теле класса можно сделать описание другого, вложенного (nested) класса. А во вложенном классе можно снова описать вложенный, внутренний (inner) класс и т. д. Эта матрешка кажется вполне естественной, но вы уже поднаторели в написании классов, и у вас возникает масса вопросов.
Хватит вопросов, давайте разберем все по порядку.
Все вложенные классы можно разделить на вложенные классы-члены класса (member classes), описанные вне методов, и вложенные локальные классы (local classes), описанные внутри методов и/или блоков. Локальные классы, как и все локальные переменные, не являются членами класса.
Классы-члены могут быть объявлены статическим модификатором static. Поведение статических классов-членов ничем не отличается от поведения обычных классов, отличается только обращение к таким классам. Поэтому они называются вложенными классами верхнего уровня (nestee tep-level classes), хотя статические классы-члены можно вкладывать друг в друга. В них можно объявлять статические члены. Используются они обычно для того, чтобы сгруппировать вспомогательные классы вместе с основным классом.
Все нестатические вложенные классы называются внутренними (inner). В них нельзя объявлять статические члены.
Локальные классы, как и все локальные переменные, известны только в блоке, в котором они определены. Они могут быть безымянными (anonymous classes).
В листинге 2.7 рассмотрены все эти случаи.
Листинг 2.7. Вложенные классы
class Nested{
static private int pr; // Переменная pr объявленa статической
// чтобы к ней был доступ из статических классов А и АВ
String s = "Member of Nested";
// Вкладываем статический класс.
static class .А{ // Полное имя этого класса — Nested.A
private int a=pr;
String s = "Member of A";
// Во вложенньм класс А вкладываем еще один статический класс
static class AB{ // Полное имя класса — Nested.А.АВ
private int ab=pr;
String s = "Member of AB";
}
}
//В класс Nested вкладываем нестатический класс
class В{ // Полное имя этого класса — Nested.В
private int b=pr;
String s = "Member of B";
// В класс В вкладываем еще один класс
class ВС{ // Полное имя класса — Nested.В.ВС
private int bc=pr;
String s = "Member of ВС";
}
void f(final int i){ // Без слова final переменные i и j
final int j = 99; // нельзя использовать в локальном классе D
class D{ // Локальный класс D известен только внутри f()
private int d=pr;
String s = "Member of D";
void pr(){
// Обратите внимание на то, как различаются
// переменные с одним и тем же именем "s"
System.out.println(s + (i+j)); // "s" эквивалентно "this.s"
System.out.println(B.this.s);
System.out.println(Nested.this.s);
// System.out.println(AB.this.s); // Нет доступа
// System.out.println(A.this.s); // Нет доступа
}
}
D d = new D(); // Объект определяется тут же, в методе f()
d.pr(); // Объект известен только в методе f()
}
}
void m(){
new Object(){ // Создается объект безымянного класса,
// указывается конструктор его суперкласса
private int e = pr;
void g(){
System.out.println("From g()) ;
}
}.g(); // Тут же выполняется метод только что созданного объекта
}
}
public class NestedClasses{
public static void main (String [] args) {
Nested nest = new Nested(); // Последовательно раскрываются
// три матрешки
Nested.A theA = nest.new A(); // Полное имя класса и уточненная
// операция new. Но конструктор только вложенного класса
Nested.A.AB theAB = theA.new AB(); // Те же правила. Operation
// new уточняется только одним именем
Nested.В theB = nest.new B(); // Еще одна матрешка
Nested.В.ВС theBC = theB.new BC();
theB.f(999); // Методы вызываются обычным образом
nest.m();
}
}
Ну как? Поняли что-нибудь? Если вы все поняли и готовы применять эти конструкции в своих программах, значит вы — выдающийся талант и можете перейти к следующему пункту. Если вы ничего не поняли, значит вы — нормальный человек. Помните принцип KISS и используйте вложенные классы как можно реже.
Для остальных дадим пояснения.
Введение вложенных классов сильно усложнило синтаксис и поставило много задач разработчикам языка. That's not all. Дотошный читатель уже зарядил новую обойму вопросов.
Механизм вложенных классов станет понятнее, если посмотреть, какие файлы с байт-кодами создал компилятор:
Компилятор разложил матрешки и, как всегда, создал отдельные файлы для каждого класса. При этом, поскольку в идентификаторах недопустимы точки, компилятор заменил их знаками доллара. Для безымянного класса компилятор придумал имя. Локальный класс компилятор пометил номером.
Оказывается, вложенные классы существуют только на уровне исходного кода. Виртуальная машина Java ничего не знает о вложенных классах. Она работает с обычными внешними классами. Для взаимодействия объектов вложенных классов компилятор вставляет в них специальные закрытые поля. Поэтому в локальных классах можно использовать только константы объемлющего метода, т. е. переменные, помеченные словом final . Виртуальная машина просто не догадается передавать изменяющиеся значения переменных в локальный класс. Таким образом не имеет смысла помечать вложенные классы private , все равно они выходят на самый внешний уровень.
All these questions can not be taken into his head. Nested classes are a direct violation of the KISS principle, and in Java they are used only in their simplest form, mainly when handling events occurring during mouse and keyboard actions.
In what cases to create nested classes? In the theory of OOP, the problem of creating nested classes is solved when considering the relations "to be a part" and "to be".
Relationship "to be part" and "be"
Now we have two different class hierarchies. One hierarchy forms the inheritance of classes, the other - the nesting of classes.
Having determined which classes will be written in your program, and how many will be, think about how to design the interaction of classes? Grow a magnificent family tree of heir classes or paint a nested class nested doll?
OOP theory advises first of all to find out in what respect your classes p and Q are - in relation to "class Q is an instance of class p" ("a class Q is a class p") or in relation to "class Q is part of class p" ( "a class Q has a class P").
For example: "Is a dog an animal" or "A dog is part of an animal"? It is clear that the first "is-a" relationship is true, which is why we have defined the class Dog as an extension of the class Pet.
The "is-a" relationship is a "generalization-detail" relationship, a relation of greater or lesser abstraction, and it corresponds to the inheritance of classes.
The "has-a" relationship is a "whole-part" relationship, it corresponds to an attachment.
Conclusion
After reading this chapter, you got an idea of the modern programming paradigm — object-oriented programming and the implementation of this paradigm in the Java language. If you are interested in the PLO, refer to the special literature [3, 4, 5, 6].
It does not matter if you do not immediately understand the principles of the PLO. It takes time and practice to develop an "object" view of programming. The second and third parts of the book will give you this practice. But first you need to familiarize yourself with the important concepts of the language Java - packages and interfaces.
Comments
To leave a comment
Object oriented programming
Terms: Object oriented programming