Java Language
наследование
Поиск…
Вступление
Наследование - это базовая объектно-ориентированная функция, в которой один класс приобретает и расширяет свойства другого класса, используя ключевое слово extends
. Для интерфейсов и implements
ключевых слов см. Интерфейсы .
Синтаксис
- класс ClassB расширяет ClassA {...}
- класс ClassB реализует InterfaceA {...}
- интерфейс InterfaceB расширяет интерфейсA {...}
- класс ClassB расширяет ClassA реализует InterfaceC, InterfaceD {...}
- абстрактный класс AbstractClassB расширяет ClassA {...}
- абстрактный класс AbstractClassB расширяет AbstractClassA {...}
- абстрактный класс AbstractClassB расширяет ClassA реализует InterfaceC, InterfaceD {...}
замечания
Наследование часто сочетается с дженериками, поэтому базовый класс имеет один или несколько параметров типа. См. Создание общего класса .
Абстрактные классы
Абстрактным классом является класс, помеченный ключевым словом abstract
. Он, вопреки не-абстрактному классу, может содержать абстрактные методы без реализации. Однако справедливо создание абстрактного класса без абстрактных методов.
Абстрактный класс не может быть создан. Он может быть подклассифицирован (расширен), пока подкласс является либо абстрактным, либо реализует все методы, помеченные как абстрактные суперклассы.
Пример абстрактного класса:
public abstract class Component {
private int x, y;
public setPosition(int x, int y) {
this.x = x;
this.y = y;
}
public abstract void render();
}
Класс должен быть отмечен абстрактным, если он имеет хотя бы один абстрактный метод. Абстрактным методом является метод, который не имеет реализации. Другие методы могут быть объявлены в абстрактном классе, который имеет реализацию, чтобы обеспечить общий код для любых подклассов.
Попытка создать экземпляр этого класса приведет к ошибке компиляции:
//error: Component is abstract; cannot be instantiated
Component myComponent = new Component();
Однако класс, который расширяет Component
, и обеспечивает реализацию для всех его абстрактных методов и может быть создан.
public class Button extends Component {
@Override
public void render() {
//render a button
}
}
public class TextBox extends Component {
@Override
public void render() {
//render a textbox
}
}
Экземпляры наследующих классов также могут быть представлены как родительский класс (нормальное наследование), и они обеспечивают полиморфный эффект при вызове абстрактного метода.
Component myButton = new Button();
Component myTextBox = new TextBox();
myButton.render(); //renders a button
myTextBox.render(); //renders a text box
Абстрактные классы vs Интерфейсы
Абстрактные классы и интерфейсы обеспечивают способ определения сигнатур методов, в то же время требуя, чтобы класс расширения / реализации обеспечивал реализацию.
Существует два основных различия между абстрактными классами и интерфейсами:
- Класс может распространять только один класс, но может реализовывать множество интерфейсов.
- Абстрактный класс может содержать экземпляры (
static
) поля, но интерфейсы могут содержать толькоstatic
поля.
Методы, объявленные в интерфейсах, не могут содержать реализации, поэтому абстрактные классы использовались, когда было полезно предоставить дополнительные методы, реализация которых называется абстрактными методами.
Java 8 позволяет интерфейсам содержать методы по умолчанию , обычно реализуемые с использованием других методов интерфейса , что делает интерфейсы и абстрактные классы одинаково мощными в этом отношении.
Анонимные подклассы абстрактных классов
В качестве удобства java позволяет создавать экземпляры анонимных подклассов абстрактных классов, которые обеспечивают реализацию абстрактных методов при создании нового объекта. Используя приведенный выше пример, это может выглядеть так:
Component myAnonymousComponent = new Component() {
@Override
public void render() {
// render a quick 1-time use component
}
}
Статическое наследование
Статический метод может быть унаследован аналогично обычным методам, однако в отличие от обычных методов невозможно создать « абстрактные » методы, чтобы заставить статический метод переопределить. Написание метода с той же сигнатурой, что и статический метод в суперклассе, представляется формой переопределения, но на самом деле это просто создает новую функцию, которая скрывает другую.
public class BaseClass {
public static int num = 5;
public static void sayHello() {
System.out.println("Hello");
}
public static void main(String[] args) {
BaseClass.sayHello();
System.out.println("BaseClass's num: " + BaseClass.num);
SubClass.sayHello();
//This will be different than the above statement's output, since it runs
//A different method
SubClass.sayHello(true);
StaticOverride.sayHello();
System.out.println("StaticOverride's num: " + StaticOverride.num);
}
}
public class SubClass extends BaseClass {
//Inherits the sayHello function, but does not override it
public static void sayHello(boolean test) {
System.out.println("Hey");
}
}
public static class StaticOverride extends BaseClass {
//Hides the num field from BaseClass
//You can even change the type, since this doesn't affect the signature
public static String num = "test";
//Cannot use @Override annotation, since this is static
//This overrides the sayHello method from BaseClass
public static void sayHello() {
System.out.println("Static says Hi");
}
}
Запуск любого из этих классов дает результат:
Hello
BaseClass's num: 5
Hello
Hey
Static says Hi
StaticOverride's num: test
Обратите внимание, что в отличие от обычного наследования методы статического наследования не скрыты. Вы всегда можете вызвать базовый метод sayHello
, используя BaseClass.sayHello()
. Но классы наследуют статические методы, если в подклассе не найдены методы с одной и той же сигнатурой. Если разные сигнатуры метода различаются, оба метода можно запускать из подкласса, даже если имя одного и того же.
Статические поля скрыть друг друга аналогичным образом.
Использование «final» для ограничения наследования и переопределения
Финальные классы
При использовании в объявлении class
final
модификатор запрещает объявлять другие классы, extend
класс. final
класс - это «лист» в иерархии классов наследования.
// This declares a final class
final class MyFinalClass {
/* some code */
}
// Compilation error: cannot inherit from final MyFinalClass
class MySubClass extends MyFinalClass {
/* more code */
}
Варианты использования для финальных классов
Заключительные классы можно комбинировать с private
конструктором для управления или предотвращения создания экземпляра класса. Это можно использовать для создания так называемого «класса утилиты», который определяет только статические элементы; т.е. константы и статические методы.
public final class UtilityClass {
// Private constructor to replace the default visible constructor
private UtilityClass() {}
// Static members can still be used as usual
public static int doSomethingCool() {
return 123;
}
}
Неизменяемые классы также должны быть объявлены final
. (Неизменяемый класс - это тот, чьи экземпляры не могут быть изменены после их создания, см. Тему I mmutable Objects .) При этом вы не можете создать изменяемый подкласс неизменяемого класса. Это нарушит Принцип замещения Лискова, который требует, чтобы подтип должен подчиняться «поведенческому контракту» его супертипов.
С практической точки зрения, объявляя неизменный класс final
легче рассуждать о поведении программы. Он также рассматривает проблемы безопасности в сценарии, где ненадежный код выполняется в изолированной программной среде. (Например, поскольку String
объявляется final
, доверенный класс не должен беспокоиться о том, что его можно обмануть в принятии изменяемого подкласса, который ненадежный вызывающий абонент мог бы тайно изменить.)
Одним из недостатков final
классов является то, что они не работают с некоторыми насмешливыми фреймворками, такими как Mockito. Обновление: версия Mockito 2 теперь поддерживает насмешку над финальными классами.
Конечные методы
final
модификатор также может применяться к методам предотвращения их переопределения в подклассах:
public class MyClassWithFinalMethod {
public final void someMethod() {
}
}
public class MySubClass extends MyClassWithFinalMethod {
@Override
public void someMethod() { // Compiler error (overridden method is final)
}
}
Конечные методы обычно используются, когда вы хотите ограничить то, что подкласс может изменить в классе, без полного запрещения подклассов.
final
модификатор также может применяться к переменным, но значение final
для переменных не связано с наследованием.
Принцип замещения Лискова
Подменяемость - это принцип в объектно-ориентированном программировании, введенный Барбарой Лисковым в майне конференции 1987 года, в которой говорится, что если класс B
является подклассом класса A
, то везде, где ожидается A
, вместо B
можно использовать B
:
class A {...}
class B extends A {...}
public void method(A obj) {...}
A a = new B(); // Assignment OK
method(new B()); // Passing as parameter OK
Это также применяется, когда тип является интерфейсом, где нет необходимости в иерархической взаимосвязи между объектами:
interface Foo {
void bar();
}
class A implements Foo {
void bar() {...}
}
class B implements Foo {
void bar() {...}
}
List<Foo> foos = new ArrayList<>();
foos.add(new A()); // OK
foos.add(new B()); // OK
Теперь список содержит объекты, которые не принадлежат к одной и той же иерархии классов.
наследование
С использованием ключевого слова extends
среди классов все свойства суперкласса (также называемые родительским классом или базовым классом ) присутствуют в подклассе (также известном как дочерний класс или производный класс )
public class BaseClass {
public void baseMethod(){
System.out.println("Doing base class stuff");
}
}
public class SubClass extends BaseClass {
}
Экземпляры SubClass
унаследовали метод baseMethod()
:
SubClass s = new SubClass();
s.baseMethod(); //Valid, prints "Doing base class stuff"
Дополнительный контент может быть добавлен в подкласс. Это позволяет использовать дополнительные функции в подклассе без каких-либо изменений в базовом классе или любых других подклассах из того же базового класса:
public class Subclass2 extends BaseClass {
public void anotherMethod() {
System.out.println("Doing subclass2 stuff");
}
}
Subclass2 s2 = new Subclass2();
s2.baseMethod(); //Still valid , prints "Doing base class stuff"
s2.anotherMethod(); //Also valid, prints "Doing subclass2 stuff"
Поля также унаследованы:
public class BaseClassWithField {
public int x;
}
public class SubClassWithField extends BaseClassWithField {
public SubClassWithField(int x) {
this.x = x; //Can access fields
}
}
private
поля и методы все еще существуют в подклассе, но недоступны:
public class BaseClassWithPrivateField {
private int x = 5;
public int getX() {
return x;
}
}
public class SubClassInheritsPrivateField extends BaseClassWithPrivateField {
public void printX() {
System.out.println(x); //Illegal, can't access private field x
System.out.println(getX()); //Legal, prints 5
}
}
SubClassInheritsPrivateField s = new SubClassInheritsPrivateField();
int x = s.getX(); //x will have a value of 5.
В Java каждый класс может распространяться не более чем на один класс.
public class A{}
public class B{}
public class ExtendsTwoClasses extends A, B {} //Illegal
Это известно как множественное наследование, и, хотя оно является законным на некоторых языках, Java не разрешает его с классами.
В результате этого у каждого класса есть неразветвленная цепочка предков классов, ведущих к Object
, из которого все классы спускаются.
Наследование и статические методы
В Java родительский и дочерний классы могут иметь статические методы с тем же именем. Но в таких случаях реализация статического метода в дочерней среде скрывает реализацию родительского класса, это не метод переопределения. Например:
class StaticMethodTest {
// static method and inheritance
public static void main(String[] args) {
Parent p = new Child();
p.staticMethod(); // prints Inside Parent
((Child) p).staticMethod(); // prints Inside Child
}
static class Parent {
public static void staticMethod() {
System.out.println("Inside Parent");
}
}
static class Child extends Parent {
public static void staticMethod() {
System.out.println("Inside Child");
}
}
}
Статические методы привязываются к классу не к экземпляру, и привязка этого метода происходит во время компиляции. Так как в первом обращении к staticMethod()
, родительская ссылка на класс p
был использован, Parent
версия «х staticMethod()
вызывается. Во втором случае, мы клали p
в Child
класс, Child
«s staticMethod()
выполняются.
Переменная затенение
Переменные SHADOWED и методы OVERRIDDEN. Какая переменная будет использоваться, зависит от класса, объявленного переменной. Какой метод будет использоваться, зависит от фактического класса объекта, на который ссылается переменная.
class Car {
public int gearRatio = 8;
public String accelerate() {
return "Accelerate : Car";
}
}
class SportsCar extends Car {
public int gearRatio = 9;
public String accelerate() {
return "Accelerate : SportsCar";
}
public void test() {
}
public static void main(String[] args) {
Car car = new SportsCar();
System.out.println(car.gearRatio + " " + car.accelerate());
// will print out 8 Accelerate : SportsCar
}
}
Сужение и расширение ссылок на объекты
Передача экземпляра базового класса в подкласс, как в: b = (B) a;
называется сужением (поскольку вы пытаетесь сузить объект базового класса до более конкретного объекта класса) и нуждается в явном типе.
Передача экземпляра подкласса базовому классу, как в: A a = b;
называется расширением и не нуждается в типе.
Для иллюстрации рассмотрим следующие объявления классов и тестовый код:
class Vehicle {
}
class Car extends Vehicle {
}
class Truck extends Vehicle {
}
class MotorCycle extends Vehicle {
}
class Test {
public static void main(String[] args) {
Vehicle vehicle = new Car();
Car car = new Car();
vehicle = car; // is valid, no cast needed
Car c = vehicle // not valid
Car c = (Car) vehicle; //valid
}
}
Заявление Vehicle vehicle = new Car();
является допустимым оператором Java. Каждый экземпляр Car
также является Vehicle
. Следовательно, назначение является законным без необходимости явного приведения типов.
С другой стороны, Car c = vehicle;
не является действительным. Статическим типом vehicle
является Vehicle
средство, которое означает, что оно может ссылаться на экземпляр Car
, грузовика ,
мотоцикла , or any other current or future subclass of
транспортного средства . (Or indeed, an instance of
автомобиля itself, since we did not declare it as an
абстрактным class.) The assignment cannot be allowed, since that might lead to
автомобиль referring to a
экземпляр Truck.
Чтобы предотвратить эту ситуацию, нам нужно добавить явный тип-cast:
Car c = (Car) vehicle;
Тип-cast говорит компилятору, что мы ожидаем, что стоимость vehicle
будет Car
или подклассом Car
. При необходимости компилятор вставляет код для выполнения проверки типа времени выполнения. Если проверка ClassCastException
неудачей, тогда при выполнении ClassCastException
будет ClassCastException
.
Обратите внимание, что не все титровальные выражения действительны. Например:
String s = (String) vehicle; // not valid
Компилятор Java знает, что экземпляр типа, совместимый с Vehicle
никогда не может быть совместимым со String
. Тип-литье никогда не будет успешным, и JLS обязывает, что это дает ошибку компиляции.
Программирование на интерфейс
Идея программирования для интерфейса состоит в том, чтобы основывать код в основном на интерфейсах и использовать только конкретные классы во время создания экземпляра. В этом контексте хороший код, касающийся, например, наборов Java, будет выглядеть примерно так (не то, что сам метод вообще никому не нужен, просто иллюстрация):
public <T> Set<T> toSet(Collection<T> collection) {
return Sets.newHashSet(collection);
}
в то время как плохой код может выглядеть так:
public <T> HashSet<T> toSet(ArrayList<T> collection) {
return Sets.newHashSet(collection);
}
Его результаты могут быть более совместимы с кодом, предоставляемым другими разработчиками, которые в целом придерживаются концепции программирования для интерфейса. Однако наиболее важными причинами использования первых являются:
- в большинстве случаев контекст, в котором используется результат, не требует и не нуждается в том, чтобы многие детали были представлены в конкретной реализации;
- присоединение к интерфейсу создает более чистый код и меньше хаков, таких как еще один общедоступный метод, добавляется в класс, обслуживающий определенный сценарий;
- код более подвержен тестированию, так как интерфейсы легко макетируются;
- наконец, концепция помогает, даже если ожидается только одна реализация (по крайней мере, для проверки).
Итак, как можно легко применить концепцию программирования к интерфейсу при написании нового кода, имея в виду одну конкретную реализацию? Один из вариантов, который мы обычно используем, представляет собой комбинацию следующих шаблонов:
- программирование на интерфейс
- завод
- строитель
Следующий пример, основанный на этих принципах, представляет собой упрощенную и усеченную версию реализации RPC, написанную для нескольких разных протоколов:
public interface RemoteInvoker {
<RQ, RS> CompletableFuture<RS> invoke(RQ request, Class<RS> responseClass);
}
Вышеупомянутый интерфейс не должен создаваться непосредственно через фабрику, вместо этого мы выводим еще более конкретные интерфейсы: один для HTTP-вызова и один для AMQP, каждый из которых имеет фабрику и строитель для создания экземпляров, которые, в свою очередь, также являются экземплярами приведенный выше интерфейс:
public interface AmqpInvoker extends RemoteInvoker {
static AmqpInvokerBuilder with(String instanceId, ConnectionFactory factory) {
return new AmqpInvokerBuilder(instanceId, factory);
}
}
Экземпляры RemoteInvoker
для использования с AMQP теперь могут быть сконструированы так легко, как (или более задействованы в зависимости от компоновщика):
RemoteInvoker invoker = AmqpInvoker.with(instanceId, factory)
.requestRouter(router)
.build();
И вызов запроса так же просто, как:
Response res = invoker.invoke(new Request(data), Response.class).get();
Из-за того, что Java 8 разрешает размещение статических методов непосредственно в интерфейсах, промежуточный завод становится неявным в вышеупомянутом коде, замененном на AmqpInvoker.with()
. В Java до версии 8 такой же эффект может быть достигнут с помощью внутреннего класса Factory
:
public interface AmqpInvoker extends RemoteInvoker {
class Factory {
public static AmqpInvokerBuilder with(String instanceId, ConnectionFactory factory) {
return new AmqpInvokerBuilder(instanceId, factory);
}
}
}
Соответствующая инстанция затем превратится в:
RemoteInvoker invoker = AmqpInvoker.Factory.with(instanceId, factory)
.requestRouter(router)
.build();
Строитель, используемый выше, может выглядеть так (хотя это упрощение, так как фактическое позволяет определить до 15 параметров, отклоняющихся от значений по умолчанию). Обратите внимание, что конструкция не является общедоступной, поэтому ее можно использовать только с вышеупомянутого интерфейса AmqpInvoker
:
public class AmqpInvokerBuilder {
...
AmqpInvokerBuilder(String instanceId, ConnectionFactory factory) {
this.instanceId = instanceId;
this.factory = factory;
}
public AmqpInvokerBuilder requestRouter(RequestRouter requestRouter) {
this.requestRouter = requestRouter;
return this;
}
public AmqpInvoker build() throws TimeoutException, IOException {
return new AmqpInvokerImpl(instanceId, factory, requestRouter);
}
}
Как правило, построитель также может быть сгенерирован с использованием инструмента, такого как FreeBuilder.
Наконец, стандартная (и единственная ожидаемая) реализация этого интерфейса определяется как локальный класс пакета для обеспечения использования интерфейса, фабрики и строителя:
class AmqpInvokerImpl implements AmqpInvoker {
AmqpInvokerImpl(String instanceId, ConnectionFactory factory, RequestRouter requestRouter) {
...
}
@Override
public <RQ, RS> CompletableFuture<RS> invoke(final RQ request, final Class<RS> respClass) {
...
}
}
Между тем, эта модель оказалась очень эффективной при разработке всего нашего нового кода, независимо от того, насколько проста или сложна функциональность.
Абстрактный класс и использование интерфейса: отношение «Is-a» vs «Has-a»
Когда использовать абстрактные классы: реализовать одно и то же или другое поведение среди нескольких связанных объектов
Когда использовать интерфейсы: реализовать контракт несколькими несвязанными объектами
Абстрактные классы создают «это» отношения, в то время как интерфейсы предоставляют «возможность».
Это можно увидеть в приведенном ниже коде:
public class InterfaceAndAbstractClassDemo{
public static void main(String args[]){
Dog dog = new Dog("Jack",16);
Cat cat = new Cat("Joe",20);
System.out.println("Dog:"+dog);
System.out.println("Cat:"+cat);
dog.remember();
dog.protectOwner();
Learn dl = dog;
dl.learn();
cat.remember();
cat.protectOwner();
Climb c = cat;
c.climb();
Man man = new Man("Ravindra",40);
System.out.println(man);
Climb cm = man;
cm.climb();
Think t = man;
t.think();
Learn l = man;
l.learn();
Apply a = man;
a.apply();
}
}
abstract class Animal{
String name;
int lifeExpentency;
public Animal(String name,int lifeExpentency ){
this.name = name;
this.lifeExpentency=lifeExpentency;
}
public abstract void remember();
public abstract void protectOwner();
public String toString(){
return this.getClass().getSimpleName()+":"+name+":"+lifeExpentency;
}
}
class Dog extends Animal implements Learn{
public Dog(String name,int age){
super(name,age);
}
public void remember(){
System.out.println(this.getClass().getSimpleName()+" can remember for 5 minutes");
}
public void protectOwner(){
System.out.println(this.getClass().getSimpleName()+ " will protect owner");
}
public void learn(){
System.out.println(this.getClass().getSimpleName()+ " can learn:");
}
}
class Cat extends Animal implements Climb {
public Cat(String name,int age){
super(name,age);
}
public void remember(){
System.out.println(this.getClass().getSimpleName() + " can remember for 16 hours");
}
public void protectOwner(){
System.out.println(this.getClass().getSimpleName()+ " won't protect owner");
}
public void climb(){
System.out.println(this.getClass().getSimpleName()+ " can climb");
}
}
interface Climb{
void climb();
}
interface Think {
void think();
}
interface Learn {
void learn();
}
interface Apply{
void apply();
}
class Man implements Think,Learn,Apply,Climb{
String name;
int age;
public Man(String name,int age){
this.name = name;
this.age = age;
}
public void think(){
System.out.println("I can think:"+this.getClass().getSimpleName());
}
public void learn(){
System.out.println("I can learn:"+this.getClass().getSimpleName());
}
public void apply(){
System.out.println("I can apply:"+this.getClass().getSimpleName());
}
public void climb(){
System.out.println("I can climb:"+this.getClass().getSimpleName());
}
public String toString(){
return "Man :"+name+":Age:"+age;
}
}
выход:
Dog:Dog:Jack:16
Cat:Cat:Joe:20
Dog can remember for 5 minutes
Dog will protect owner
Dog can learn:
Cat can remember for 16 hours
Cat won't protect owner
Cat can climb
Man :Ravindra:Age:40
I can climb:Man
I can think:Man
I can learn:Man
I can apply:Man
Ключевые заметки:
Animal
- это абстрактный класс с общими атрибутами:name
и жизнь.lifeExpectancy
и абстрактные методы:remember()
иprotectOwner()
.Dog
иCat
- этоAnimals
, которые внедрили методыremember()
иprotectOwner()
.Cat
можетclimb()
ноDog
не может.Dog
можетthink()
ноCat
не может. Эти специфические возможности добавляются вCat
иDog
путем реализации.Man
неAnimal
но он можетThink
,Learn
,Apply
иClimb
.Cat
неMan
но он можетClimb
.Dog
неMan
но она можетLearn
Man
является ниCat
ниDog
но может иметь некоторые из возможностей последних двух, не расширяяAnimal
,Cat
илиDog
. Это делается с интерфейсами.Несмотря на то, что
Animal
является абстрактным классом, он имеет конструктор, в отличие от интерфейса.
TL; DR:
Несвязанные классы могут иметь возможности через интерфейсы, но связанные классы меняют поведение посредством расширения базовых классов.
Обратитесь к странице документации Java, чтобы понять, какой из них следует использовать в конкретном случае использования.
Рассмотрим использование абстрактных классов, если ...
- Вы хотите поделиться кодом между несколькими тесно связанными классами.
- Вы ожидаете, что классы, которые расширяют ваш абстрактный класс, имеют много общих методов или полей или требуют модификаторов доступа, кроме публичных (например, защищенных и приватных).
- Вы хотите объявить нестатические или нефинальные поля.
Рассмотрите возможность использования интерфейсов, если ...
- Вы ожидаете, что не связанные классы будут реализовывать ваш интерфейс. Например, многие несвязанные объекты могут реализовать интерфейс
Serializable
. - Вы хотите указать поведение конкретного типа данных, но не обеспокоены тем, кто реализует его поведение.
- Вы хотите использовать множественное наследование типа.
Перекрытие в наследовании
Overriding in Inheritance используется, когда вы используете уже определенный метод из суперкласса в подклассе, но по-другому, чем то, как метод был первоначально разработан в суперклассе. Переопределение позволяет пользователю повторно использовать код, используя существующий материал и модифицируя его в соответствии с потребностями пользователя.
В следующем примере показано, как ClassB
переопределяет функциональность ClassA
, изменяя то, что отправляется через метод печати:
Пример:
public static void main(String[] args) {
ClassA a = new ClassA();
ClassA b = new ClassB();
a.printing();
b.printing();
}
class ClassA {
public void printing() {
System.out.println("A");
}
}
class ClassB extends ClassA {
public void printing() {
System.out.println("B");
}
}
Выход:
В