Одним из важнейших свойств любого языка программирования является использование имен. Когда мы создаем объект, он получает имя области хранения. Имя метода представляет собой конкретное действие. Описывая свою систему с помощью имен, мы делаем её более понятной для людей, что облегчает понимание и модификацию программы. Это очень похоже на написание эссе — цель которого состоит в том, чтобы связаться с читателем.
Мы используем имена для ссылок или описания всех объектов и методов. Если имена выбраны правильно, это помогает нам и другим людям лучше понять наш код.При попытках "представления" концепций, имеющих тонкие различия между собой, из естественного языка в язык программирования возникают специфические проблемы. В повседневной жизни мы используем одно и то же слово для выражения различных значений — то есть "перегрузки". Мы говорим "стираю рубашку", "стираю машину" и "стираю собаку". Однако если бы нас заставили говорить так: "стирка рубашки рубашка", "стирка машины машина" и "стирка собаки собака", это выглядело бы глупо. Это потому, что слушатель не нуждается в четко определённой разнице действий. Большинство естественных языков имеют высокую степень "избыточности", поэтому даже при отсутствии нескольких слов можно сделать вывод о значении. У нас нет необходимости иметь уникальные идентификаторы — мы можем заключить значение из конкретного контекста.Большинство языков программирования (особенно C) требуют от нас установки уникального идентификатора для каждого функционального блока. Поэтому абсолютно невозможно использовать один метод print()
для вывода целых чисел и другой метод print()
для вывода чисел с плавающей запятой — каждый метод должен иметь уникальное название.В Java существует ещё одна причина, которая заставляет методы иметь перегрузку: конструкторы. Поскольку имя конструктора определяется именем класса, может существовать только один конструктор с данным именем. Но что делать, если мы хотим создавать объекты несколькими способами? Например, предположим, что мы хотим создать класс, который будет инициализироваться стандартным образом, а также из файла. В этом случае нам потребуются два конструктора: один без параметров (по умолчанию) и второй, принимающий строку как параметр — имя файла, используемого для инициализации объекта. Так как они все являются конструкторами, они должны иметь одно и то же имя, то есть имя класса. Таким образом, чтобы позволить одному и тому же методу использоваться с различными типами параметров, "перегрузка методов" является крайне важной концепцией. При этом, хотя перегрузка методов необходима для конструкторов, она также применима ко всем остальным методам и удобна в использовании. В данном примере мы одновременно демонстрируем перегрузку конструкторов и обычных методов:```markdown
//: Overloading.java
// Demonstration of overloaded constructors and regular methods.
import java.util.*;
class Tree { int height; Tree() { prt("Planting a sapling"); height = 0; } Tree(int i) { prt("Creating a new tree with height " + i + " feet"); height = i; } void info() { prt("The tree has a height of " + height + " feet"); } void info(String s) { prt(s + ": The tree has a height of " + height + " feet"); } static void prt(String s) { System.out.println(s); } }
public class Overloading { public static void main(String[] args) { for(int i = 0; i < 5; i++) { Tree t = new Tree(i); t.info(); t.info("overloaded method"); } // Overloaded constructor: new Tree(); } } ///:~
Класс Tree
может быть создан либо как саженец без каких-либо параметров, либо как растущее дерево в теплице. Для поддержки этих вариантов создания используются два конструктора — один без параметров (такие конструкторы называют "по умолчанию"), а другой принимает готовую высоту.
Примечание ①: В некоторых материалах Sun по Java такие конструкторы называются "конструкторами без параметров" (no-arg constructors). Однако термин "конструктор по умолчанию" используется уже много лет, поэтому я выбрал его.
```Метод info()
также может быть вызван различными способами. Например, если нам требуется вывести дополнительное сообщение, то используем параметр типа `String`; если этого не требуется, то просто вызываем метод без него. Поскольку для двух различных вариантов использования одного и того же имени метода, это может показаться странным. К счастью, перегрузка методов позволяет использовать одно имя для обоих случаев.### 4.2.1 Определение перегруженных методов
Как Java понимает, какой именно метод мы имеем в виду, когда они имеют одинаковое имя? Здесь есть простое правило: каждый перегруженный метод должен иметь уникальный список типов параметров.
Если немного поразмышлять, можно задуматься над тем, как программист может отличить два метода с одинаковым именем, кроме как по типам параметров?
Даже порядок параметров достаточно для разделения двух методов (хотя обычно мы стараемся избегать этого подхода, поскольку он приводит к трудному в обслуживании коду):
//: OverloadingOrder.java
// Перегрузка на основе порядка
// аргументов.
public class OverloadingOrder {
static void print(String s, int i) {
System.out.println(
"Строка: " + s +
", целое число: " + i);
}
static void print(int i, String s) {
System.out.println(
"Целое число: " + i +
", строка: " + s);
}
public static void main(String[] args) {
print("Первая строка", 11);
print(99, "Первое целое число");
}
} ///:~
Два метода print()
имеют одинаковые параметры, но различаются порядком этих параметров, что позволяет отличить их друг от друга.
Основные (представительные) типы могут автоматически преобразовываться в более "широкие" типы. Это может немного усложнять ситуацию при использовании перегрузки. В следующем примере показано, как происходит передача основных типов в перегруженные методы:```markdown //: PrimitiveOverloading.java // Преобразование примитивных типов и перегрузка
public class PrimitiveOverloading { // Логический тип не может быть автоматически преобразован static void prt(String s) { System.out.println(s); }
void f1(char x) { prt("f1(char)"); } void f1(byte x) { prt("f1(byte)"); } void f1(short x) { prt("f1(short)"); } void f1(int x) { prt("f1(int)"); } void f1(long x) { prt("f1(long)"); } void f1(float x) { prt("f1(float)"); } void f1(double x) { prt("f1(double)"); }
void f2(byte x) { prt("f2(byte)"); } void f2(short x) { prt("f2(short)"); } void f2(int x) { prt("f2(int)"); } void f2(long x) { prt("f2(long)"); } void f2(float x) { prt("f2(float)"); } void f2(double x) { prt("f2(double)"); }
void f3(short x) { prt("f3(short)"); } void f3(int x) { prt("f3(int)"); } void f3(long x) { prt("f3(long)"); } void f3(float x) { prt("f3(float)"); } void f3(double x) { prt("f3(double)"); }
void f4(int x) { prt("f4(int)"); } void f4(long x) { prt("f4(long)"); } void f4(float x) { prt("f4(float)"); } void f4(double x) { prt("f4(double)"); }
void f5(long x) { prt("f5(long)"); } void f5(float x) { prt("f5(float)"); } void f5(double x) { prt("f5(double)"); }
void f6(float x) { prt("f6(float)"); } void f6(double x) { prt("f6(double)"); } }
## При рассмотрении вывода этого программного кода можно заметить, что константное значение 5 рассматривается как значение типа `int`. Поэтому если бы была доступна перегруженная версия метода, то она использовалась бы для значения типа `int`. В остальных случаях, если тип данных "меньше" аргумента в перегруженном методе, происходит приведение типа. Для типа `char` поведение немного отличается, поскольку если нет точной совпадающей версии метода для типа `char`, он преобразуется в тип `int`.
```
```java
void testConstVal() {
prt("Проверка с 5");
f1(5); f2(5); f3(5); f4(5); f5(5); f6(5); f7(5);
}
void testChar() {
char x = 'x';
prt("Указатель типа char:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
void testByte() {
byte x = 0;
prt("Указатель типа byte:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
void testShort() {
short x = 0;
prt("Указатель типа short:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
void testInt() {
int x = 0;
prt("Указатель типа int:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
void testLong() {
long x = 0;
prt("Указатель типа long:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
void testFloat() {
float x = 0;
prt("Указатель типа float:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
void testDouble() {
double x = 0;
prt("Указатель типа double:");
f1(x); f2(x); f3(x); f4(x); f5(x); f6(x); f7(x);
}
public static void main(String[] args) {
PrimitiveOverloading p = new PrimitiveOverloading();
p.testConstVal();
p.testChar();
p.testByte();
p.testShort();
p.testInt();
p.testLong();
p.testFloat();
p.testDouble();
}
}
///:~
```А что произойдет, если наш аргумент "больше", чем ожидалось в перегруженном методе? Изменённый вариант программы демонстрирует это:
```java
//: Demotion.java
// Приведение примитивных типов и перегрузка методов
public class Demotion {
static void prt(String s) {
System.out.println(s);
}
void f1(char x) { prt("f1(char)"); }
void f1(byte x) { prt("f1(byte)"); }
void f1(short x) { prt("f1(short)"); }
void f1(int x) { prt("f1(int)"); }
void f1(long x) { prt("f1(long)"); }
void f1(float x) { prt("f1(float)"); }
void f1(double x) { prt("f1(double)"); }
void f2(char x) { prt("f2(char)"); }
void f2(byte x) { prt("f2(byte)"); }
void f2(short x) { prt("f2(short)"); }
void f2(int x) { prt("f2(int)"); }
void f2(long x) { prt("f2(long)"); }
void f2(float x) { prt("f2(float)"); }
void f3(char x) { prt("f3(char)"); }
void f3(byte x) { prt("f3(byte)"); }
void f3(short x) { prt("f3(short)"); }
void f3(int x) { prt("f3(int)"); }
void f3(long x) { prt("f3(long)"); }
void f4(char x) { prt("f4(char)"); }
void f4(byte x) { prt("f4(byte)"); }
void f4(short x) { prt("f4(short)"); }
void f4(int x) { prt("f4(int)"); }
void f5(char x) { prt("f5(char)"); }
void f5(byte x) { prt("f5(byte)"); }
void f5(short x) { prt("f5(short)"); }
void f6(char x) { prt("f6(char)"); }
void f6(byte x) { prt("f6(byte)"); }
void f7(char x) { prt("f7(char)"); }
void testDouble() {
double x = 0;
prt("double аргумент:");
f1(x); f2((float)x); f3((long)x); f4((int)x);
f5((short)x); f6((byte)x); f7((char)x);
}
public static void main(String[] args) {
Demotion p = new Demotion();
p.testDouble();
}
} ///:~
```Здесь метод принимает значения меньшего размера и меньшего диапазона. Если наш аргумент имеет больший диапазон, его следует явно преобразовать в нужный тип с помощью скобок. В противном случае компилятор сообщит об ошибке. Обратите внимание, что это "сужающее преобразование". Это значит, что в процессе преобразования или трансформации могут быть потеряны некоторые данные. Именно поэтому компилятор заставляет нас явно определять такие преобразования — мы должны четко указывать желаемое преобразование.## 4.2.3 Переопределение возвращаемых значений
Можно запутаться относительно следующих вопросов: почему методы перечислены только с названием класса и параметрами метода? Почему методы не различаются по типу возвращаемого значения? Например, вот два метода с одинаковым названием и параметрами:
```cpp
void f() {}
int f() {}
```
Если компилятор может точно определить значение на основе контекста (например, `int x = f()`), то это вполне допустимо. Однако мы можем вызвать метод, игнорируя его возвращаемое значение; обычно это называют "вызовом метода ради побочных эффектов", так как нам важна не сама возвращаемая величина, а другие последствия вызова метода. Поэтому если мы вызываем метод таким образом:
```cpp
f();
```
Как Java определяет конкретный способ вызова `f()`? Как другие люди смогут распознать и понять этот код? Из-за таких проблем нельзя использовать тип возвращаемого значения для различения перегруженных методов.
## 4.2.4 По умолчанию конструктор
Как было отмечено ранее, по умолчанию конструктор не имеет параметров. Его задача — создание "пустого объекта". Если класс создан без конструктора, компилятор автоматически создаст конструктор по умолчанию. Например:
```java
//: DefaultConstructor.java
class Bird {
int i;
}
public class DefaultConstructor {
public static void main(String[] args) {
Bird nc = new Bird(); // по умолчанию!
}
} ///:~
```Для строки:
```java
new Bird();
```
Задача этой строки — создать новый объект и вызвать конструктор по умолчанию, даже если он ещё не был явно определён. Без него нет бы никакого способа вызова, чтобы создать наш объект. Однако, если уже определён какой-либо конструктор (будь то с параметрами или без), компилятор не будет генерировать его автоматически:
```java
class Bush {
Bush(int i) {}
Bush(double d) {}
}
```
Теперь, если используем такой код:
```java
new Bush();
```
Компилятор сообщит, что не может найти подходящий конструктор. Будто бы мы вообще не определили ни одного конструктора, и компилятор говорит: "Похоже, вам нужен конструктор, давайте создадим его для вас." Но если вы определили хотя бы один конструктор, компилятор скажет: "Ага, вы определили конструктор, теперь я знаю, чего вы хотите; если вы не добавили по умолчанию, значит, вы хотели его пропустить."
## 4.2.5 Ключевое слово `this`
Если есть два объекта одного типа, называемых `a` и `b`, вы можете не знать, как одновременно вызвать метод `f()` для обоих этих объектов:
```java
class Banana { void f(int i) { /* ... */ } }
Banana a = new Banana(), b = new Banana();
a.f(1);
b.f(2);
```
Как может этот единственный метод `f()` понять, что он был вызван для `a` или для `b`?Чтобы писать код в удобном, объектно-ориентированном виде — то есть «отправлять сообщение объекту» — компилятор выполняет некоторую работу за кадром. Секрет заключается в том, что первый аргумент передается методу `f()`, и этот аргумент представляет собой ссылку на тот объект, который будет использоваться при выполнении этого метода. Таким образом, вышеупомянутые два вызова метода преобразуются следующим образом:```java
Banana.f(a,1);
Banana.f(b,2);
```
Это внутреннее представление, которое мы не можем использовать явно, но благодаря которому можно понять, что происходит за сценой.
Предположим, что мы внутри метода и хотим получить ссылку на текущий объект. Поскольку эта ссылка передается компилятором «тайно», нет доступного имени для этой ссылки. Однако существует специальное ключевое слово для этой цели: `this`. Ключевое слово `this` (обратите внимание, что его можно использовать только внутри метода) создает ссылку на текущий объект, для которого был вызван метод. Эта ссылка может использоваться так же, как любая другая ссылка на объект. Но обратите внимание, если вы хотите вызвать метод того же класса из другого метода этого класса, вам не обязательно использовать `this`. Достаточно просто вызвать метод. Текущее значение `this` автоматически применяется к другим методам. Поэтому мы можем использовать такой код:
```java
class Apricot {
void pick() { /* ... */ }
void pit() { pick(); /* ... */ }
}
```
Внутри `pit()` мы могли бы написать `this.pick()`, но это было бы лишним. Компилятор сделает это автоматически. Ключевое слово `this` используется только в тех случаях, когда требуется явное использование ссылки на текущий объект. Например, если вы хотите вернуть ссылку на текущий объект, часто это делается через оператор `return`.
```java
//: Leaf.java
// Простое использование ключевого слова "this"
public class Leaf {
private int i = 0;
Leaf increment() {
i++;
return this;
}
void print() {
System.out.println("i = " + i);
}
public static void main(String[] args) {
Leaf x = new Leaf();
x.increment().increment().increment().print();
}
} ///:~
```
Поскольку метод `increment()` возвращает ссылку на текущий объект через ключевое слово `this`, это позволяет легко выполнять несколько операций над одним и тем же объектом.
(1) Вызов конструктора из конструктора
Если для одного класса объявлено несколько конструкторов, часто требуется вызывать один конструктор из другого, чтобы избежать повторения кода. Это можно сделать с помощью ключевого слова `this`.
Обычно, когда мы говорим о `this`, имеется в виду «этот объект» или «текущий объект». Кроме того, он сам создает ссылку на текущий объект. Внутри конструктора, если передать ему список аргументов, то ключевое слово `this` будет иметь другое значение: оно явно вызывает конструктор с таким списком аргументов. Таким образом, мы можем вызвать другой конструктор непосредственно. Пример:
```java
//: Flower.java
// Вызов конструкторов с помощью "this"
``````java
public class Flower {
private int petalCount = 0;
private String s = new String("null");
Flower(int petals) {
petalCount = petals;
System.out.println(
"Конструктор с целочисленным аргументом, petalCount= "
+ petalCount);
}
Flower(String ss) {
System.out.println(
"Конструктор с строковым аргументом, s=" + ss);
s = ss;
}
Flower(String s, int petals) {
this(petals);
//! this(s); // Нельзя вызвать два!
this.s = s; // Ещё одно использование "this"
System.out.println("Строковый и целочисленный аргументы");
}
Flower() {
this("hi", 47);
System.out.println(
"По умолчанию конструктор (без аргументов)");
}
void print() {
//! this(11); // Только внутри конструктора!
System.out.println(
"petalCount = " + petalCount + " s = " + s);
}
public static void main(String[] args) {
Flower x = new Flower();
x.print();
}
}
///:~
```
```Здесь конструктор `Flower(String s, int petals)` демонстрирует проблему: хотя можно вызвать один конструктор через `this`, нельзя вызвать два. Кроме того, вызов конструктора должен быть первым действием, иначе компилятор выдаст ошибку.
Этот пример также показывает ещё одну функцию `this`. Поскольку имя параметра `s` совпадает с именем члена данных `s`, возникает путаница. Для решения этой проблемы используется `this.s` для обращения к члену данных. Такой подход часто встречается в Java-коде, и этот способ широко применяется в данной книге. В методе `print()`, мы обнаруживаем, что компилятор не позволяет нам вызывать конструктор из любого метода, кроме одного конструктора.
(2) Смысл слова `static`
Поняв ключевое слово `this`, мы можем более полно понять смысл `static` (статического) метода. Это означает, что конкретный метод не имеет доступа к `this`. Мы не можем вызвать нестатический метод из статического метода (замечание ②), хотя обратное возможно. Кроме того, без создания объекта можно обращаться к статическому методу через сам класс. В действительности, это является основной сущностью статических методов. Они как бы эквивалентны глобальным функциям (в C-языке). Исключение составляет то, что глобальные функции недопустимы в Java; если поместить статический метод внутрь класса, он сможет обращаться к другим статическим методам и полям.②: Одним из случаев, когда такие вызовы возможны, является передача ссылки на объект в статический метод. Затем, используя эту ссылку (которая фактически является `this`), можно вызывать нестатические методы и обращаться к нестатическим полям. Однако обычно лучше создать обычный, нестатический метод.
Некоторые люди жалуются, что статические методы не являются "ориентированными на объекты", так как они имеют некоторые характеристики глобальных функций; при использовании статических методов нет необходимости отправлять сообщение объекту, поскольку отсутствует `this`. Это может быть очевидным аргументом, если вы обнаруживаете, что используете большое количество статических методов, стоит переоценить свою стратегию. Тем не менее, концепция статического метода очень полезна, и часто требуется её применение. Поэтому вопрос о том, являются ли они действительно "ориентированными на объекты", следует оставить для теоретиков. На самом деле, даже Smalltalk имеет что-то похожее на статическое свойство в своих "классовых методах".
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Опубликовать ( 0 )