第6章 接口、lambda表达式与内部类

Java与C++异

  1. Java内部类还有一个隐式引用,指向实例化这个对象的外部类对象。Java静态内部类没有这个附加的指针,相当于C++中的嵌套类。

Java与C++同

接口

接口概念

  1. 接口不是类。而是对希望符合这个接口的类的一组需求。

不能使用new运算符实例化一个接口:x = new Comparable(. . .); // ERROR
尽管不能构造接口的对象,却能声明接口的变量:Comparable x; // OK
接口变量必须引用实现了这个接口的类对象:x = new Employee (. . .); //OK provided Employee implements Comparable
使用instanceof检查一个对象是否实现了某个特定的接口:if (anObject instanceof Comparable) { . . . }

  1. 接口中的所有方法都自动是public方法。因此,在接口中声明方法时,不必提供关键字public。实现类的实现方法也一定是public。
  2. 接口绝不会有实例字段,在Java8之前,接口中绝对不会实现方法。
  3. 虽然在接口中不能包含实例字段,但是可以包含常量。与接口中的方法都自动被设置为public—样,接口中的字段总是自动public static final。
  4. 接口之间可以继承。
  5. 每个类只能有一个超类,但却可以实现多个接口。

接口与抽象类

  • 为什么要用接口,不直接用抽象类?

使用抽象类表示通用属性存在一个严重的问题。每个类只能扩展一个类。假 设Employee类已经扩展了另一个类,例如Person,它就不能再扩展第二个类了。class Employee extends Person, Comparable // ERROR
接口可以提供多重继承的大多数好处,同时还能避免多重继承的复杂性和低效性。

静态和私有方法

  1. 在Java8中,允许在接口中增加静态方法,合法,但违于将接口作为抽象规范的初衷。通常的做法都是将静态方法放在伴随类中。在标准库中,你会看到成对出现的接口和实用工具类,如Collection/Collections 或 Path/Paths。
  2. 在Java9中,接口中的方法可以是private,private方法可以是静态方法或实例方法。由于私有方法只能在接口本身的方法中使用,所以它们的用法很有限,只能作为接口中其他方法的辅助方法。

默认方法

  1. 为接口方法提供一个默认实现。必须用default修饰符标记这样一个方法。
  2. 默认方法可以调用其他方法。例如,Collection接口可以定义一个便利方法:
public interface Collection
{
int size(); // an abstract method
default boolean isEmpty() { return size() == 0; }
}
  1. 默认方法的一个重要用法是“接口演化”,为接口增加一个非默认方法不能保证“源代码兼容”。

解决默认方法冲突

在一个接口中将一个方法定义为默认方法,然后又在超类或另一个接口中定义同样的方法,会发生什么情况?
1.超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
2.接口冲突。如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型(不论是否是默认参数)相同的方法,必须覆盖这个方法来解决冲突。
在这里插入图片描述
在这里插入图片描述

接口与回调(不明白)

回调:可以指定某个特定事件发生时应该采取的动作。
在很多程序设计语言中,可以提供一个函数名,定时器要定期地调用这个函数。但是,Java标准类库中的类采用的是面向对象方法。你可以向定时器传人某个类的对象,然后,定时器调用这个对象的方法。由于对象可以携带一些附加的信息,所以传递一个对象比传递一个函数要灵活得多。

package corejava.v1ch06.timer;

/**
   @version 1.02 2017-12-14
   @author Cay Horstmann
*/

import java.awt.*;
import java.awt.event.*;
import java.time.*;
import javax.swing.*;

public class TimerTest
{  
   public static void main(String[] args)
   {  
      var listener = new TimePrinter();

      // construct a timer that calls the listener
      // once every second
      var timer = new Timer(1000, listener);
      timer.start();

      // keep program running until the user selects "OK"
      JOptionPane.showMessageDialog(null, "Quit program?");
      System.exit(0);
   }
}

class TimePrinter implements ActionListener
{  
   public void actionPerformed(ActionEvent event)
   {  
      System.out.println("At the tone, the time is " 
         + Instant.ofEpochMilli(event.getWhen()));
      Toolkit.getDefaultToolkit().beep();
   }
}

Comparator接口

为什么有比较器Comparator接口

Arrays类中的sort方法承诺可以对对象数组进行排序,但要求满足下面这个条件:对象所属的类必须实现Comparable接口,任何实现Comparable接口的类都需要实现compareTo方法。

// Comparable接口只有compareTo一个方法
 public interface Comparable<T> {
    public int compareTo(T o);
}
class Employee implements Comparable<Employee>
{
	public int compareTo(Employee other)
	{
		return Double.compare(salary, other.salary);
	}
}

需求:
现在假设我们希望按长度递增的顺序对字符串进行排序,而不是按字典顺序进行排序。肯定不能让String类用两种不同的方式实现compa reTo方法,更何况,String类也不应由我们来修改。
要处理这种情况,Arrays.sort方法还有第二个版本,有一个数组和一个比较器(comparator)作为参数,比较器是实现了 Comparator接口类的实例

public interface Comparator<T>
{
int compare(T first, T second);
}

实现步骤

  1. 要按长度比较字符串,可以如下定义一个实现Comparator的类bog:
class LengthComparator implements Comparator<String>
{
public int compare(String first, String second)
{
return first.length() - second.length();
}
}
  1. 具体完成比较时,需要建立一个实例:
var comp = new LengthConparator();
if (comp.compare(words[i], words[j]) > 0)...

将这个调用与words[ i ]. compareTo (words[ j ])进行比较。这个compare方法要在比较器对象上
调用,而不是在字符串本身上调用。

  1. 要对一个数组排序,需要为Arrays. sort方法传人一个Lengthcomparator对象:
LengthComparator lengthComparator = new LengthComparator();
String[] friendString= {"ccc","bb","a"};
Arrays.sort(friendString,lengthComparator);

尽管Lengthcomparator对象没有状态,不过还是需要建立这个对象的一个实例。我们需要这个实例朱调用compare方法——它不是一个静态方法。
在6.2节中我们会了解,利用lambda表达式可以更容易地使用Comparator。

示例

import java.util.Arrays;
import java.util.Comparator;

public class LengthComparator implements Comparator<String> {

	public LengthComparator() {
		// TODO Auto-generated constructor stub
	}
	
	public int compare(String t1,String t2)
	{
		return t1.length()-t2.length();
	}
	
	public static void main(String[] args) {
		LengthComparator lengthComparator = new LengthComparator();
		String[] friendString= {"ccc","bb","a"};
		Arrays.sort(friendString,lengthComparator);
		for(String s:friendString)
		{
			System.out.println(s);
		}	
	}
}

在这里插入图片描述

问题

  1. Comparator有两个抽象方法,但实现类可只实现compare方法

因为任何一个类,一定继承了Object类,Object类实现了equals方法。所以无论是否覆写Object的equals方法,任何一个类都实现了equals方法。

  1. 为什么Comparator接口有两个抽象方法compare和equals,Comparator还是一个函数式接口(@FunctionalInterface)

根据Java语言规范的定义,一个使用了该注释的接口类型声明将被视为一个函数式接口。从概念上讲,一个函数式接口有且只有一个抽象方法。由于默认方法已经有了实现,所以它们不是抽象方法。如果一个接口中声明的抽象方法是重写了超类Object类中任意一个public方法,那么这些抽象方法并不会算入接口的抽象方法数量中。因为任何接口的实现都会从其父类Object或其它地方获得这些方法的实现。
注意:函数式接口的实现可以由Lambda表达式、方法引用、构造器引用等方式实现。
如果一个类型使用了该注释,那么编译器将会生成一个错误信息,除非这个类型是一个接口类型,而不是一个注释类型、枚举或类。同时使用该注释的接口满足函数式接口的要求,即一个函数式接口有且只有一个抽象方法。
但是编译器会将所有定义为函数式接口(满足函数式接口要求)的接口视为函数式接口,而不管这个接口声明中是否使用了函数式接口的注释(即@FunctionalInterface)。
从中我们可以知道:

  • 一个函数式接口有且只有一个抽象方法。
  • 默认方法不是抽象方法,因为它们已经实现了。
  • 重写了超类Object类中任意一个public方法的方法并不算接口中的抽象方法。
    所以虽然Comparator接口中有两个抽象方法compare和equals,但equals并不算入接口中的抽象方法,所以Comparator接口还是满足函数式接口的要求,Comparator接口是一个函数式接口。

————————————————
版权声明:本文为CSDN博主「H_X_P」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/H_X_P_/article/details/105030682

对象克隆

为什么克隆

如果不克隆,原变量和副本都是同一个对象的引用,副本的改变会影响原变量。

 var original = new Employee("John Public", 50000);
  Employee copy = original;
  copy.raiseSalary(10); // oops--also changed original

克隆:copy的初始状态与original 相同,但是之后它们各自会有自己不同的状态

 var original = new Employee("John Public", 50000);
  Employee copy = original.clone();
  copy.raiseSalary(10); // OK--original unchanged

浅拷贝:Object.clone方法的问题

  • clone方法是Object的一个protected方法,只能逐个字段地进行拷贝。如果对象所有字段都是基本数据类型,完全OK。
  • 但如果对象包含子对象的引用,拷贝字段就会得到相同子对象的另一个引用,这样一来,原对象和克隆的对象仍然会共享一些信息。
  • 如果原对象和浅克隆对象共享的子对象是不可变的,那么这种共享就是安全的。如果子对象属于一个不可变的类,如String ,就是这种情况。或者在对象的生命期中,子对象一直包含不变的常量,没有更改器方法会改变它,也没有方法会生成它的引用,这种情况下同样是安全的。
  • 不过,通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝(deepcopy),
    同时克隆所有子对象。
    在这里插入图片描述
 public static void main(String[] args) throws CloneNotSupportedException
   {
      var original = new Employee("John Q. Public", 50000);
      original.setHireDay(2000, 1, 1);
      System.out.println("浅克隆前 original = "+original);
      Employee copy = original.clone();
      copy.setName("new name");
      copy.raiseSalary(10);
      copy.setHireDay(2002, 12, 31);
      System.out.println("浅克隆后 original = " + original);
      System.out.println("浅克隆后 copy = " + copy);
   }

在这里插入图片描述

基本数据类型的salary和不变类String的name浅拷贝完全OK,但是可变类LocalDate的hireDay,copy的改变依旧带来original的改变。

深拷贝

对于每一个类,尽量不要用clone,先考虑:

  1. 默认的clone方法是否满足要求;
  2. 是否可以在可变的子对象上调用clone来修补默认的clone方法;
    如果实现clone(),必须:
    1 实现 Cloneable 接 口;
  1. 对象对于克隆很“偏执”,如果一个对象请求克隆,但是没有实现这个接口,就会生成一个检查型异常。
  2. Cloneable接口是Java提供的少数标记接口(tagging interface)之一。标记接口不包含任何方法;它唯一的作用就是允许在类型查询中使用instanceof:if (obj instanceof Cloneable)
    在这里插入图片描述
  1. 重新定义clone方法,并指定public访问修饰符。
// 下面来看创建深拷贝的clone方法的一个例子:
class Employee implements Cloneable
{
 public Employee clone() throws CloneNotSupportedException
   {
      // call Object.clone()
      Employee cloned = (Employee) super.clone();
      // clone mutable fields
      cloned.hireDay = (Date) hireDay.clone();
      return cloned;
   }
}

还可以用对象串化(Serializable接口)实现深拷贝。很容易实现,但效率不高。

数组的深拷贝

所有数组类型都有一个public的clone方法,而不是受保护的。可以用这个方法建立一个新数组,包含原数组所有元素的副本。例如:

   int[] luckyNumbers = { 2, 3, 5, 7, 11, 13 };
      int[] cloned = luckyNumbers.clone();
      cloned[5] = 12; // doesn't change luckyNumbers[5]

lambda表达式

为什么引入lambda表达式

定义

lambda表达式是一个可传递的代码块,可以在以后执行一次或多次

需求

在Arrays.sort()中通过Comparator接口实现类完成排序时,sort方法会一直多次调用实现类的compare方法,直到对象数组中所有元素有序。
此过程中,将一个代码块传递到某个对象(一个定时器,或者一个sort方法)。这个代码块会在将来某个时间调用。到目前为止,在Java中传递一个代码段并不容易,你不能直接传递代码段。Java是一种面向对象语言,所以必须构造一个对象,这个对象的类需要有一个方法包含所需的代码

lambda表达式的语法

参数,箭头(->)以及一个表达式

	(String first, String second)
	-> first.length() - second.length()
  1. 多个表达式用{},并包含显示return语句。
    	(String first, String second)->
    	{
    	if (first.length() < second.length()) return -1;
    	else if (first.length() > second.length()) return 1;
    	else return 0;
    	};
  1. 即使lambda表达式没有参数,仍然要提供空括号,就像无参数方法一样:
()-> { for (int i = 100; i >= 0; i--) System.out.println(i); }

3.如果可以推导出一个lambda表达式的参数类型,则可以忽略参数类型。如果方法只有一个参数,而且这个参数的类型可以推导得出,那么甚至还可以省略小括号:

ActionListener listener = event ->
    	System.out.println("The time is "
    	+ Instant.ofEpochMiUi(event.getWhen()));
    	// instead of (event) -> ... or (ActionEvent event)-> ...
  1. 无须指定lambda表达式的返回类型。lambda表达式的返回类型总是会由上下文推导得出。例如,下面的表达式可以在需要int类型结果的上下文中使用。
	(String first, String second)
	-> first.length() - second.length()
  1. 如果一个lambda表达式只在某些分支返回一个值,而另外一些分支不返回值,这是不合法的。例如,(int x) -> { if (x >= 0) return 1; }就不合法。

函数式接口

Java中lambda表达式能且只能转换为函数接口

对于只有一个抽象方法的接口,称为函数式接口(@FunctionalInterface代码标注)。
当需要函数式接口对象时,可以用lambda表达式替代
(Java中lambda表达式能且只能转换为函数接口,甚至不能把lambda表达式赋给类型为Object的变量,Object不是函数式接口。)

考虑Arrays.sort方法。它的第二个参数需要一个Comparator对象,Comparator就是只有一个方法的接口,所以可以提供一个lambda表达式:

Arrays.sort(words,
    			(first, second) -> first.length() - second.length();

原理: 采用lambda表达式后,在底层,Arrays.sort方法会接收实现了 Comparator<String>的某个类的对象。在这个对象上调用compare方法会执行这个lambda表达式的体

优点:

  1. 这些对象和类的管理完全取决于具体实现,与使用传统的内联类相比,这样可能要高效得多。
  2. 把lambda表达式看作是一个函数,而不是一个对象。

常见的函数式接口

  1. java.util.function包中有一个尤其有用的接口 Predicate<T>:
public interface Predicate<T>
{
boolean test(T t);
// additional default and static methods
}

ArrayList类有一个removelf方法,它的参数就是一个Predicate。这个接口专门用来传递lambda表达式。例如,下面的语句将从一个数组列表删除所有null值:
list.removelf(e -> e = null);

  1. 另一个有用的函数式接口是Supplier<T>:
public interface Supplier<T>
{
T get()}

供应者(supplier)没有参数,调用时会生成一个T类型的值。供应者用于实现懒计算。
例如,考虑以下调用:

LocalDate hireDay = Objects.requireNonNullOrElse(day, new LocalDate(1970, 1, 1));

这不是最优的。我们预计day很少为null,所以希望只在必要时才构造默认的LocalDate。通过使用供应者,我们就能延迟这个计算:
LocalDate hireDay = Objects.requireNonNullOrElseGet(day, ()-> new LocalDate(1970, 1, 1)); Objects.requireNonNullOrElseGet() 方法只在需要值时才调用供应者。

个人理解

  1. 前提条件:只有在需要调用某个函数式接口时,才能用lambda表达式。

Arrays.sort方法编译器知道第二个参数是一个Comparator实现类对象,看到lambda表达式,会自动把lambda表达式转换成函数式接口实现类对象。

方法引用

举例

假设你希望只要出现一个定时器事件就打印这个事件对象。当然,为此也可以调用:
var timer = new Timer(1000, event -> System.out.println(event));

但是,如果直接把println方法传递到Timer构造器就更好了。具体做法如下:
var timer = new Timer(1000, System.out::println);

表达式System.out: :println是一个方法引用(method reference),它指示编译器生成一个函数式接口的实例,覆盖这个接口的抽象方法来调用给定的方法。在这个例子中,会生成一个ActionListener,它的 actionPerformed(ActionEvent e)方法要调用 System.out.println(e)。

再来看一个例子,假设你想对字符串进行排序,而不考虑字母的大小写。可以传递以下方法表达式:
Arrays.sort(strings, String::compareToIgnoreCase)

在这里插入图片描述
从这些例子可以看出,要用::运算符分隔方法名与对象或类名。主要有3种情况

1. object::instanceMethod
2. Class:: instanceMethod
3. Class::staticMethod

在第1种情况下,方法引用等价于向方法传递参数的lambda表达式。对于System.out: :println,对象是 System.out,所以方法表达式等价于 x -> System.out.println(x)
对于第2种情况,第1个参数会成为方法的隐式参数。例如,String::compareToIgnoreCase等同于(x, y) ->x.compareToIgnoreCase(y)
在第3种情况下,所有参数都传递到静态方法:Math::pow等价于(x, y) ->Math.pow(x, y)

方法引用语法,不太懂

  1. 只有当lambda表达式的体只调用一个方法而不做其他操作时,才能把lambda表达式重写为方法引用。考虑以下lambda表达式:

s -> s.length() == 0,这里有一个方法调用。但是还有一个比较,所以这里不能使用方法引用。

  1. 如果有多个同名的重载方法,编译器就会尝试从上下文中找出你指的是哪一个方法。例如,Math.max方法有两个版本,一个用于整数,另一个用于double值。选择哪一个版本取决于Math::max转换为哪个函数式接口的方法参数。
  2. 类似于lambda表达式,方法引用不能独立存在,总是会转换为函数式接口的实例
  3. 有时API包含一些专门用作方法引用的方法。例如,Objects类有一个方法isNull,用于测试一个对象引用是否为null。乍看上去这好像没有什么用,因为测试obj=nuU比Objects.isNuU(obj)更有可读性。不过可以把方法引用传递到任何有Predicate参数的方法。例如,要从一个列表删除所有null引用,就可以调用:
list.removelf(Objects::isNull);
//A bit easier to read than list.removelf(e->e==null); 
  1. 包含对象的方法引用与等价的lambda表达式还有一个细微的差别。考虑一个方法引用,如 separator::equals。 如果 separator 为 null,构造 separator::equals 时就会立即抛出一个 NullPointerException 异常。lambda 表达式 x-> separator.equals(x)只在调用时才会抛出 NuUPointerException。
  2. 可以在方法引用中使用this参数。例如,this:: equals等同于x -> this .equals (x)。使用super也是合法的。下面的方法表达式
    super:: instanceMethod使用this作为目标,会调用给定方法的超类版本。
    在这里插入图片描述

构造器的引用,不太懂

构造器引用与方法引用很类似,只不过方法名为new。例如,Person::new是Person构造器的一个引用。哪一个构造器呢?这取决于上下文。假设你有一个字符串列表。可以把它转换为一个Person对象数组,为此要在各个字符串上调用构造器,调用如下:

ArrayList<String> names = ...;
Stream<Person> stream = names.stream().map(Person::new);
List<Person> people = st ream.collect(Collecto rs.toList());

变量作用域

捕获自由变量

你可能希望能够在lambda表达式中访问外围方法或类中的变量。

  public static void repeatMessage(String text, int delay)
    {
    ActionListener listener = event ->
    {
    System.out.println(text);
    Toolkit.getDefaultToolkit().beep();
    };
    new Timer(delay, listener).start();
    }

来看这样一个调用:

repeatMessage("Hello", 1000); // prints Hello every 1,000 milliseconds

现在来看lambda表达式中的变量text。注意这个变量并不是在这个lambda表达式中定义的。实际上,这是repeatMessage方法的一个参数变量。问题,lambda表达式的代码可能在repeatMessage调用返回很久以后才运行,而那时这个参数变量已经不存在了。如何保留text变量呢?

lambda表达式有3个部分:

  1. 一个代码块;
  2. 参数;
  3. 自由变量的值,这是指非参数而且不在代码中定义的变量。

在我们的例子中,这个lambda表达式有一个自由变量text。表示lambda表达式的数据结构必须存储自由变量的值,在这里就是字符串"Hello"。我们说它被lambda表达式捕获 (captured)。lambda表达式可以捕获外围作用域中变量的值。

关于代码块以及自由变量值有一个术语:闭包(closure)。在Java中,lambda表达式就是闭包。

捕获的自由变量值不能改变

  1. 只能引用值不会改变的变量。这个限制是有原因的。如果在lambda表达式中更改变量,并发执行多个动作时就会不
    安全。例如,下面的做法是不合法的:
    在这里插入图片描述
  2. 如果在lambda表达式中引用一个变量,而这个变量可能在外部改变,这也是不合法的。例如,下面就是不合法的:
  public static void repeat(String text, int count)
    {
    for (int i = 1; i <= count; i++)
    {
    ActionListener listener = event ->
    {
    System.out.println(i + ": " + text);
    // ERROR: Cannot refer to changing i
    };
    new Timer(1000, listener).start();
    }
    }
  1. :lambda表达式中捕获的变量必须实际上是事实最终变量(effectively final)。事实最终变量是指,这个变量初始化之后就不会再为它赋新值。在这里,text总是指示同一个String对象,所以捕获这个变量是合法的。不过,i的值会改变,因此不能捕获i。

变量不能同名

  1. 在一个方法中,不能有两个同名的局部变量,因此,lambda表达式中同样也不能有同名的局部变量。
  2. 在一个lambda表达式中使用this关键字时,是指创建这个lambda表达式的方法的this参数。例如,考虑下面的代码:
public class Application
{
public void init()
{
ActionListener listener = event ->
{
System.out.println(this.toString());
}
}
}

表达式this.toString()会调用Application对象的toString方法,而不是ActionListener实例的方法。

处理lambda表达式

延迟执行

使用lambda表达式的重点是延迟执行(deferred execution)。毕竟,如果想要立即执行代码,完全可以直接执行,而无须把它包装在一个lambda表达式中。之所以希望以后再执行代码,这有很多原因如:

  • 在一个单独的线程中运行代码;
  • 多次运行代码;
  • 在算法的适当位置运行代码(例如,排序中的比较操作);
  • 发生某种情况时执行代码(如,点击了一个按钮,数据到达,等等);
  • 只在必要时才运行代码。

下面来看一个简单的例子。假设你想要重复一个动作n次。将这个动作和重复次数传递到一个repeat方法:repeat(10, () -> System.out.println("Hello, World!"));要接受这个lambda表达式,需要选择(偶尔可能需要提供)一个函数式接口Runnable接口,调用action.run()时会执行这个lambda表达式的主体:

public static void repeat(int n, Runnable action)
{
for (int i = 0; i < n; i++) action.run();
}

常用函数式接口

在这里插入图片描述
现在让这个例子更复杂一些。我们希望告诉这个动作它出现在哪一次迭代中。为此,需要选择一个合适的函数式接口,其中要包含一个方法,这个方法有一个int参数而且返回类型为void。处理int值的标准接口如下:

public interface IntConsumer
{
void accept(int value);
}

下面给出reapeat方法的改进版本:

public static void repeat(int n, IntConsumer action)
{
for (int i = 0; i < n; i++) action.accept(i);
}

可以如下调用: repeat(10, i -> System.out.println("Countdown: " + (9 - i)));
表6-3列出了基本类型int 、long和double的34个可用的特殊化接口。在第8章会了解到,使用这些特殊化接口比使用通用接口更高效。
在这里插入图片描述

再谈Comparator-不懂

Comparator接口包含很多方便的静态方法来创建比较器。这些方法可以用于lambda表达式或方法引用。

  1. 静态comparing方法取一个“键提取器”函数,它将类型T映射为一个可比较的类型(如String)。对要比较的对象应用这个函数,然后对返回的键完成比较。例如,假设有一个Person对象数组,可以如下按名字对这些对象进行排序:
Arrays.sort(people, Comparator.comparing(Person::getName));

在这里插入图片描述

Comparator<Person> byAge = Comparator.comparing(person -> person.getAge());

Comparator.comparing()方法是一个静态方法,它不是抽象方法。该方法返回一个Comparator对象,该对象可以用于比较提供的键提取器函数的结果。在这种情况下,Comparator.comparing()方法使用了Lambda表达式,将一个函数作为参数传递给该方法。因此,Lambda表达式在这里被用作函数式接口的实现

内部类

内部类作用

内部类(innerclass)是定义在另一个类中的类。为什么需要使用内部类呢?主要有两个原因:

  • 内部类可以对同一个中的其他类隐藏。
  • 内部类方法可以访问定义这个类的作用域中的数据,包括原本私有的数据

内部类隐式引用访问外部对象状态

  1. TimePrinter类位于TalkingClock类内部。但并不意味着每个TalkingClock对象都有一个TimePrinter实例字段。如前所示,TimePrinter对象是由TalkingClock类的方法构造的(第X行通过new创建)。
  2. 内部类在构造器中设置对外部类的引用。编译器会修改所有的内部类构造器,添加一个对应外围类引用的参数。因为TimePrinter类没有定义构造器,所以编译器为这个类生成了一个无参数构造器,生成的代码如下所示:
public TimePrinter(TalkingClock clock) // automatically generated code
{
outer = clock;
}

再次强调,注意outer不是Java的关键字。我们只是用它说明内部类的有关机制。

  1. 在s tart方法中构造一个TimePrinter对象后,编译器就会将当前语音时钟的this引用传递给这个构造器:
var listener = new TimePrinter(this); // parameter automatically added
  1. 表达式OuterClass. this表示外围类引用。
public void actionPerformed(ActionEvent event)
{
if (TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();
}
  1. 可以采用以下语法更加明确地编写内部类对象的构造器:
    outerObject.new InnerClass(construction parameters)
    例如,
ActionListener listener = this.new TimePrinter();

在这里,最新构造的TimePrinter对象的外围类引用被设置为创建内部类对象的方法的this引用。这是一种最常见的情况。通常,this.限定词是多余的。不过,也可以通过显式地命名将外围类引用设置为其他的对象。例如,由于TimePrinter是一个公共内部类,对于任意的语音时钟都可以构造一个TimePrinter:

var jabberer = new TalkingClock(100, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();

需要注意,在外围类的作用域之外,可以这样引用内部类:OuterClass. InnerClass

package innerClass;

import java.awt.*;
import java.awt.event.*;
import java.time.*;

import javax.swing.*;

/**
 * This program demonstrates the use of inner classes.
 * @version 1.11 2017-12-14
 * @author Cay Horstmann
 */
public class InnerClassTest
{
   public static void main(String[] args)
   {
      var clock = new TalkingClock(1000, true);
      clock.start();

      // keep program running until the user selects "OK"
      JOptionPane.showMessageDialog(null, "Quit program?");
      System.exit(0);
   }
}

/**
 * A clock that prints the time in regular intervals.
 */
class TalkingClock
{
   private int interval;
   private boolean beep;

   /**
    * Constructs a talking clock
    * @param interval the interval between messages (in milliseconds)
    * @param beep true if the clock should beep
    */
   public TalkingClock(int interval, boolean beep)
   {
      this.interval = interval;
      this.beep = beep;
   }

   /**
    * Starts the clock.
    */
   public void start()
   {
      var listener = new TimePrinter();   //TimePrinter对象是由TalkingClock类的方法构造的
      var timer = new Timer(interval, listener);
      timer.start();
   }

   public class TimePrinter implements ActionListener
   {
      public void actionPerformed(ActionEvent event)
      {
         System.out.println("At the tone, the time is " 
            + Instant.ofEpochMilli(event.getWhen()));
         if (beep) Toolkit.getDefaultToolkit().beep();
      }
   }
}

Java内部类还有一个隐式引用,指向实例化这个对象的外部类对象。通过这个指针,它可以访问外部对象的全部状态。例如,在Java中,Iterator类不需要一个显式指针指向它所指的LinkedList。
在这里插入图片描述

C++有嵌套类。被嵌套的类包含在外围类的作用域内。嵌套类与Java中的内部类很类似。
在Java中,静态内部类没有这个附加的指针,所以Java的静态内部类就相当于C++中的嵌套类。

只有内部类可以是私有的,这样只有外部类可以通过方法构造内部类。而常规类可以有包可见
性或公共可见性。

内部类的特殊语法规则

表达式OuterClass. this表示外部类引用。例如,可以像下面这样编写TimePrinter内部类的actionPerformed方法:

public void actionPerformed(ActionEvent event)
{
if (TalkingClock.this.beep) Toolkit.getDefaultToolkit().beep();
}

反过来,可以采用以下语法更加明确地编写内部类对象的构造器:outerObject.new InnerClass(construction parameters)例如,ActionListener listener = this.new TimePrinter();
在这里,最新构造的TimePrinter对象的外围类引用被设置为创建内部类对象的方法的this引用。这是一种最常见的情况。通常,this.限定词是多余的。不过,也可以通过显式地命名将外围类引用设置为其他的对象。例如,由于TimePrinter是一个公共内部类,对于任意的语音时钟都可以构造一个TimePrinter:

var jabberer = new TalkingClock(100, true);
TalkingClock.TimePrinter listener = jabberer.new TimePrinter();

需要注意,在外围类的作用域之外,可以这样引用内部类:
OuterClass. InnerClass

内部类是否有用、必要和安全

  1. 内部类是一个编译器现象,与虚拟机无关。编译器将会把内部类转换为常规的类文件,用$ (美元符号)分隔外部类名与内部类名,而虚拟机则对此一无所知。
  2. 内部类可以访问外围类的私有数据,拥有更大的访问权限,所以天生就比常规类功能更加强大。

局部内部类

可以在一个方法中局部地定义这个类。
在这里插入图片描述

  • 声明局部类时不能有访问说明符(即public或private)。局部类的作用域被限定在声明这个局部类的块中。
  • 局部类有一个很大的优势,即对外部世界完全隐藏,甚至TalkingClock类中的其他代码也不能访问它。除start方法之外,没有任何方法知道TimePrinter类的存在。
  • 局部类可访问局部变量。

局部类访问局部变量

与其他内部类相比较,局部类还有一个优点。它们不仅能够访问外部类的字段,还可以访问局部变量!不过,那些局部变量必须是事实最终变量(effectively final)。
如:外部类不再存储beep和interval字段。beep和interval变成函数参数传入start,成为局部变量。
在这里插入图片描述
在这里插入图片描述

匿名内部类

匿名内部类可以继承父类或者实现接口
匿名内部类不能有构造器
[外链图片转存中…(img-f929a42888512dddbba77952f.png)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-G5nwSepZ-1684037016822)(null)]

双括号初始化

[外链图片转存中…(img-d5a48cfbd22b5b652c6cc1431)]

静态方法获得当前类的类名

在这里插入图片描述

  1. 当匿名内部类实现接口。接口没有构造函数。
  2. 当匿名内部类继承父类。匿名内部类调用父类的构造函数产生对象。

静态内部类

  • 只有内部类可以声明为static。
  • 静态内部类就类似于其他内部类,只不过静态内部类的对象没有生成它的外围类对象的引用
  • 与常规内部类不同,静态内部类可以有静态字段和方法
  1. 静态内部类不需要依赖外部类的实例,可以直接调用外部类的静态字段和静态方法,但不能访问外部类的非静态字段和非静态方法。而常规内部类必须依赖外部类的实例,才能访问外部类的字段和方法。
  2. 静态内部类通常用于封装一些与外部类相关的实用工具方法或工厂方法等。例如:

在这里插入图片描述

  • 在接口中声明的内部类自动是static和public。
class ArrayAlg
{
	public static class Pair
	{
	...
	}
...
}

非static内部类不能有static字段和方法

在这里插入图片描述
非static的内部类,在外部类加载的时候,并不会加载它,而是被实例化的时候才会加载

“if you’re going to have a static method, the whole inner class has to be static. Without doing that, you couldn’t guarantee that the inner class existed when you attempted to call the static method. ”
如果内部类没有static的话,就需要实例化内部类才能调用,说明非static的内部类不是自动跟随外部类加载的,而是被实例化的时候才会加载
而static的语义,就是外部类能直接通过内部类名来访问内部类中的static方法,而非static的内部类又是不会自动加载的,所以这时候内部类也要static,否则会前后冲突。
2.
非static的内部类,在外部类加载的时候,并不会加载它,所以它里面不能有静态变量或者静态方法。
(1) static类型的属性和方法,在类加载的时候就会存在于内存中。
(2) 要使用某个类的static属性或者方法,那么这个类必须要加载到jvm中。
基于以上两点,可以看出,如果一个非static的内部类如果具有static的属性或者方法,那么就会出现一种情况:内部类未加载,但是却试图在内存中创建static的属性和方法,这当然是错误的。原因:类还不存在,但却希望操作它的属性和方法。
————————————————
版权声明:本文为CSDN博主「三天热度」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/qq_41694349/article/details/79376819

static内部类可以有static字段和方法

在这里插入图片描述

接口、抽象类、内部类有没有构造器?

  • 接口没有构造器(接口无法实例化,接口所有字段都是public static final),抽象类有构造器(如果没有,会自动生成无参构造函数)。
  • 普通内部类,局部内部类,static内部类都有构造器,只有匿名内部类没有构造器。

代理

利用代理可以在运行时创建实现了一组给定接口的新类。只有在编译时期无法确定需要实现哪个接口时才有必要使用代理。

通配符和泛型类型

“?” 表示该类型参数可以是任何类型的通配符。这种类型通常用来表示一个方法或类可以接受任何类型的参数。在方法中,可以使用 Object 类型来代替通配符,因为 Object 类型是所有类的父类。

区别

通配符和泛型类型都是 Java 泛型中的重要概念,但它们有一些区别:

  1. 定义方式不同:通配符使用 “?” 表示,可以用在泛型类、泛型方法、泛型接口的定义中;而泛型类型使用 “” 表示,可以指定具体的类型,例如 “<String>”、“<Integer>” 等。

  2. 可以接受的类型不同:通配符表示一种不确定的类型,可以表示任意类型的对象,包括基本数据类型和引用类型;而泛型类型可以指定具体的类型,但只能包括引用类型,不能包括基本数据类型

  3. 使用场景不同:通配符通常用于表示一个方法或类可以接受任何类型的参数,而泛型类型通常用于定义具有通用性的类、接口和方法

  4. 限制不同:通配符有一些限制,例如不能用于定义泛型类型、不能用作方法的返回类型、不能用作类或接口的继承等;而泛型类型没有这些限制,可以定义泛型类型、用作方法的返回类型、用作类或接口的继承等。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值