Java基础知识扫盲(二)——接口、lambda 表达式、内部类、代理

接口

接口不是类,是对类的一组需求描述,类要遵从接口描述的统一格式进行定义。

  • 接口中所有方法都是public,不需要声明关键字public
  • 接口不能包含有实例域和静态方法
  • SE 8之后可以在接口中实现默认方法,用default修饰符标识
  • 可以把接口看成是没有实例域的抽象类
  • 可以声明接口变量,不可以实例化接口
  • instanceOf也可以检测一个对象是否实现了某个特定的接口
  • 接口中的域将被自动设为 public static final

接口中为什么新增default 方法

  • 已经存在的大多数实现体,会因为接口中新增的方法,而无法编译,但是default可以避免这些问题,保证源代码兼容
  • 实现接口时更专注于覆盖真正关心的方法。

解决默认方法冲突

  1. 超类优先。如果超类提供了一个具体方法,同名而且有相同参数类型的默认方法会被忽略。
  2. 接口冲突。 如果一个接口提供了一个默认方法,另一个接口提供了一个同名而且参数类型 (不论是否是默认参数)相同的方法, 必须覆盖这个方法来解决冲突
class Student implements Person, Named{
    ...
}

Person和Named都提供了一个getName方法,Java编译器会报告一个错误,让程序员解决这个二义性。

当然,如果两个接口都没有为共享方法提供默认实现, 那么就与 Java SE 8之前的情况一样,这里不存在冲突。 实现类可以有两个选择: 实现这个方法, 或者干脆不实现。如果是后一种情况, 这个类本身就是抽象的。

  1. 一个类扩展了一个超类,同时实现了一个接口,并从超类和接口继承了相同的方法。
    只会考虑超类方法, 接口的所有默认方法都会被忽略。

tips:千万不要让一个默认方法重新定义 Object 类中的某个方法。

例如, 不能为 toString或 equals 定义默认方法, 尽管对于 List 之类的接口这可能很有吸引力, 由于“ 类优先”规则, 这样的方法绝对无法超越 Object.toString 或 Objects.equals

回调
可以指出某个特定事件发生时应该采取的动作。例如定时任务:

public class CallBackMeth{
  
  public static void main(String[] args) {
    ActionListener listener = new TimePrinter();
    Timer t = new Timer(1000, listener);
    t.start();

    JOptionPane.showMessageDialog(null,"Quit?");
    System.exit(0);
  }
}

class TimePrinter implements ActionListener
{
  public void actionPerformed(ActionEvent event)
  {
    System.out.println("At the tone, the time is " + new Date());
    Toolkit.getDefaultToolkit().beep(); // 响铃
  }
}

Comparator 比较器
自定义字符串长度比较器:

public class StringLengthComparator {
  public static void main(String[] args) {
    Comparator<String> comparator = new LengthComparator();
    System.out.println(comparator.compare("hello", "world"));
    System.out.println("hello".compareTo("world"));
    
    String[] names = { "songyanyan", "songjunfeng", "syy", "jeff" };
    Arrays.sort(names, comparator);
    System.out.println(names);
  }
  
}

class LengthComparator implements Comparator<String> {
  @Override
  public int compare(String o1, String o2) {
    return o1.length() - o2.length();
  }
}

Cloneable 克隆实现
// clone方法是Object的一个protected方法,不可以直接调用

 * @see java.lang.Cloneable
 */
protected native Object clone() throws CloneNotSupportedException;
对象拷贝和克隆.png

默认的克隆操作是“浅克隆”。如果原对象和浅克隆对象共享的子对象是不可变的,这种共享即是安全的。例如String,或者在对象的生命周期中没有更改器方法会改变它,也没有方法会生成它的引用。但是Date是可变的,例如下面的浅拷贝示例:

浅拷贝.png

而通常子对象都是可变的,必须重新定义clone方法来建立一个深拷贝,同时克隆出所有子对象。

  • 实现 Cloneable 接口
  • 重新定义 clone 方法,并指定 public 访问修饰符
  • 必须当心子类的克隆。
@Override
public Employee clone() throws CloneNotSupportedException {
  // call Object.clone()
  Employee cloned = (Employee)  super.clone();
  // clone mutable fields 克隆可变域
  cloned.hireDay = (Date) hireDay.clone();

  return cloned;
}

所有数组类型都有一个 public 的 clone 方法, 而不是 protected: 可以用这个方法建立一个新数组, 包含原数组所有元素的副本,只适用于(一维)数组。
需要明确的是Java中不存在多维数组的说法,二维数组即为数组的数组

int[][] arrayTestCloned2 = new int[arrayTest.length][];
for (int i = 0; i< arrayTest.length; i++){
  arrayTestCloned2[i] = Arrays.copyOf(arrayTest[1],arrayTest[1].length);
}

lambda表达式

// 比较器
Comparator<String> comparator = (first, second) -> {
  return first.length() - second.length();
};

// 事件监听
ActionListener listener = e -> System.out.println("the time is " + new Date());
Timer timer = new Timer(1000, e -> System.out.println());

函数式接口
对于只有一个抽象方法的接口, 需要这种接口的对象时, 就可以提供一个 lambda 表达式。这种接口称为函数式接口 (functional interface )

@FunctionalInterface
public interface Predicate<T> {

/**
 * Evaluates this predicate on the given argument.
 *
 * @param t the input argument
 * @return {@code true} if the input argument matches the predicate,
 * otherwise {@code false}
 */
boolean test(T t);
...
}

方法引用

Timer timer1 = new Timer(1000, System.out::println);

List<String> list = new ArrayList();
list.sort(String::compareToIgnoreCase);
  • object::instanceMethod
  • Class::staticMethod
  • Class::instanceMethod
  • super::instanceMethod

构造器引用

List<Integer> nums = Arrays.asList(1, 5, 4);
Stream<Apple> appleStream = nums.stream().map(Apple::new);
Apple[] apples = appleStream.toArray(Apple[]::new);

编译器会选择有一个 Integer 参数的构造器, 因为它从上下文推导出这是在对一个整型参数调用构造器。
toArray方法调用这个构造器来得到一个正确类型的数组。然后填充这个数组并返回。

变量作用域

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

text是repeatMessage的参数而不是lambda表达式的。lambda表达式有3个部分:

  • 代码块
  • 参数
  • 自由变量的值(非参数并且不在代码块儿中定义的变量)

这个text就是lambda表达式自由变量。表示lambda表达式的数据结构一定有存储自由变量的值。

在 lambda 表达式中, 只能引用值不会改变的变量。如果在 lambda 表达式中改变变量, 并发执行多个动作时就会不安全。变量在外部改变,也是不合法的.

public static void numDown(int num, int delay) {
  for(int i=0;i<= num;i++){
    ActionListener listener = event ->{
      System.out.println(num--); //Error: 不能引用可变的变量值num
      System.out.println(i); //Error: 不可以调用可变变量i
    };
    new Timer(delay, listener).start();
  }
}
IDE相关提示.png

即lambda表达式中扑火的变量必须/实际是最终变量(这个变量初始化之后就不会再为它赋新值)

不能声明一个局部变量同名的参数/局部变量。

String first = "Hello";
Comparator<String> stringComparator = (first, second) -> first.length() - second.length(); //Error:在域中已经声明过变量first

在lambda表达式中使用this。会调用Application相应方法。lambda表达式的作用域嵌套在init方法中。lambda表达式中this的含义并没有变化。

处理lambdab表达式

重点:延迟执行。

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

调用:

repeat(10, i -> System.out.println(i));

最好使用这些特殊化规范来减少自动装箱。

基本类型的函数式接口.png
常用的函数式接口.png

大多数标准函数式接口都提供了非抽象方法来生成或合并函数。

如果设计你自己的接口,其中只有一个抽象方法,可以用 @FunctionalInterface 注解来标记这个接口。

Comparator

内部类

  • 内部类方法可以访问该类定义所在的作用域中的数据, 包括私有的数据
  • 内部类可以对同一个包中的其他类隐藏起来
  • 当想要定义一个回调函数且不想编写大量代码时,使用匿名 (anonymous) 内部类比较便捷
  • 内部类对象拥有一个对外围类对象的引用
  • 外围类的引用在构造器中设置。编译器修改了所有的内部类的构造器, 添加一个外围类引用的参数(为这个内部类生成一个默认的构造器)。
  • 在方法调用内部类之后,会创建内部类的对象,编译器会将this引用传递给当前的内部类构造器

特殊语法规则

public class LocalInnerClassTest{
   public static void main(String[] args)
   {
      TalkingClock clock = new TalkingClock();
       clock.start(1000, true);

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

class TalkingClock
{
   /**
    * Starts the clock.
    * @param interval the interval between messages (in milliseconds)
    * @param beep true if the clock should beep
    */
   public void start(int interval, boolean beep){
      class TimePrinter implements ActionListener
      {
         public void actionPerformed(ActionEvent event)
         {
            System.out.println("At the tone, the time is " + new Date());
            if (beep) Toolkit.getDefaultToolkit().beep();
         }
      }
      ActionListener listener = new TimePrinter();
      Timer t = new Timer(interval, listener);
      t.start();
   }
}
  • OuterClass.this 表示外围类引用

  • outerObject.new InnerClass(construction parameters) 明确地编写内部对象的构造器

  • OuterClass.InnerClass 外围类的作用域外,这样引用内部类

  • 内部类中声明的所有静态域都必须是 final

  • 内部类 static 方法,只能访问外围类的静态域和方法


    访问外部非静态域sum报错.png
  • 编译器将会把内部类翻译成用$美元符号分隔外部类名与内部类名的常规类文件, 而虚拟机则对此一无所知。例如,在 TalkingClock类内部的 TimePrinter 类将被翻译成类文件 TalkingClock$TimePrinter.class

public class TalkingClock$TimePrinter
{
  public TalkingClock$TimePrinter(TalkingClock);
  public void actionPerformed(java.awt.event.ActionEvent);
  final TalkingClock this$0;
}

编译器使内部类为了引用外围类, 生成了一个附加的实例域 this$0

class TalkingClock{
  private int interval;
  private boolean beep;
  public TalkingClock(int, boolean);
  static boolean access$0(TalkingClock);
  public void start();
}

TalkingClock(外围类)被编译器添加了一个静态方法access$0,它将(返回作为参数)传递给它的对象域beep(外围类的私有域)

if (beep)
if (TalkingClock.access$0(outer))

存在一定的安全问题。如果内部类访问了私有数据域, 就有可能通过附加在外围类所在包中的其他类访问它们。

  • 局部类有一个优势,即对外部世界可以完全地隐藏起来。
  • 局部类不能用 public 或 private 访问说明符进行声明。它的作用域被限定在声明这个局部
    类的块中。
  • 除 start 方法之外,没有任何方法知道 TimePrinter 类的存在

局部类可以访问局部变量(必须是事实上的final类型)
TimePrinter 类在 beep 域释放之前将 beep 域用start 方法的局部变量进行备份

查看TalkingClock$TimePrinter类

class TalkingClock$1TimePrinter{
  TalkingClock$1TimePrinter(TalkingClock, boolean);
  public void actionPerformed(java.awt.event.ActionEvent);
  final boolean val$beep;
  final TalkingClock this$0;
}
  • 注意构造器的 boolean 参数和 val$beep 实例变量。
  • 当创建一个对象的时候, beep 就会被传递给构造器,并存储在 val$beep 域中。
  • 编译器必须检测对局部变量的访问, 为每一个变量建立相应的数据域, 并将局部变量拷贝到构造器中, 以便将这些数据域初始化为局部变量的副本

局部类的方法只可以引用定义为 final 的局部变量,所以在编译之后局部变量在局部类中的拷贝为final类型。

匿名内部类

class TalkingClock{
   /**
    * Starts the clock.
    * @param interval the interval between messages (in milliseconds)
    * @param beep true if the clock should beep
    */
   public void start(int interval, boolean beep)
   {
     ActionListener listener = new ActionListener()
     { //匿名类
        public void actionPerformed(ActionEvent event){
           System.out.println("At the tone, the time is " + new Date());
           if (beep) Toolkit.getDefaultToolkit().beep();
        }
     };
      Timer t = new Timer(interval, listener);
      t.start();
   }
}
  • 匿名类不能有构造器。取而代之的是,将构造器参数传递给超类 ( superclass) 构造器
Person queen = new Person("Mary");
// a Person object
Person count = new Person("Dracula") { . . . }
// an object of an inner class extending Person
// 如果构造参数的闭小括号后面跟一个开大括号, 正在定义的就是匿名内部类

构造一个数组列表:

new ArrayList<String>() 
{// 匿名子类
  {// 调用外层类ArrayList的方法add 构造对象块
    add("Harry");
    add("Tony");
  }
};

调用 getClass 时调用的是 this.getClass(),而
静态方法没有 this,获取该静态方法的类:

new Object(){}.getCIass().getEnclosingClass() // gets class of static method

new Object(){}会建立 Object 的一个匿名子类的一个匿名对象,getEnclosingClass则得到其外围类, 也就是包含这个静态方法的类

静态内部类

public class StaticInnerClassTest{
   public static void main(String[] args){
      double[] d = new double[20];
      for (int i = 0; i < d.length; i++)
         d[i] = 100 * Math.random();
      ArrayAlg.Pair p = ArrayAlg.minmax(d);
      System.out.println("min = " + p.getFirst());
      System.out.println("max = " + p.getSecond());
   }
}

class ArrayAlg{
   public static class Pair{
      private double first;
      private double second;

      public Pair(double f, double s){
         first = f;
         second = s;
      }

      public double getFirst(){
         return first;
      }

      public double getSecond(){
         return second;
      }
   }

   /**
    * Computes both the minimum and the maximum of an array
    * @param values an array of floating-point numbers
    * @return a pair whose first element is the minimum and whose second element
    * is the maximum
    */
   public static Pair minmax(double[] values){
      double min = Double.POSITIVE_INFINITY;
      double max = Double.NEGATIVE_INFINITY;
      for (double v : values){
         if (min > v) min = v;
         if (max < v) max = v;
      }
      return new Pair(min, max);
   }
}
  • 在内部类不需要访问外围类对象的时候, 应该使用静态内部类
  • 与常规内部类不同,静态内部类可以有静态域和方法
  • 声明在接口中的内部类自动成为 static 和 public 类

代理(proxy)

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

代理类可以在运行时创建全新的类。这样的代理类能够实现指定的接口。具备的方法:

  • 指定接口所需要的全部方法
  • Object 类中的全部方法, 例如, toString、 equals 等

不能在运行时定义这些方法的新代码。要提供一个调用处理器(invocationhandler)。调用处理器是实现了 InvocationHandler 接口的类对象

InvocationHandler handler = new InvocationHandler() {
  @Override 
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    ...
    return ...;
  }
}

创建代理对象
需要使用Proxy.newProxyInstance方法,三个参数:

public static Object newProxyInstance(ClassLoader loader,
                                      Class<?>[] interfaces,
                                      InvocationHandler h)
  • 一个类加载器。目前,用null表示使用默认的类加载器
  • 一个Class对象数组。每个元素都是需要实现的接口
  • 一个调用处理器。

如何定义一个处理器? 能够用结果代理对象做些什么?

  • 路由对远程服务器的方法调用
  • 在程序运行期间,将用户接口事件与动作关联起来
  • 为调试, 跟踪方法调用

使用代理和调用处理器跟踪方法调用。

public class ProxyTest {
  public static void main(String[] args) {
    Object[] objects = new Object[500];
    Class[] interfaces = new Class[] { Comparable.class, Iterable.class };

    for (int i = 0; i < objects.length; i++) {
      Integer value = i + 1;
      ClassTraceHandler handler = new ClassTraceHandler(value);
      objects[i] = Proxy.newProxyInstance(null, interfaces, handler);
    }

    Integer key = new Random().nextInt(objects.length) + 1;
    int result = Arrays.binarySearch(objects, key);
    if (result >= 0)
      System.out.println(objects[result]);

  }
}

class ClassTraceHandler implements InvocationHandler {
  private Object target;

  public ClassTraceHandler(Object t) {
    target = t;
  }

  @Override
  public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
    System.out.print(target);
    System.out.print("." + method.getName() + "(");
    if (args != null) {
      for (int i = 0; i < args.length; i++) {
        System.out.print(args[i]);
        if (i < args.length - 1)
          System.out.print(", ");
      }
    }
    System.out.println(")");

    return method.invoke(target, args);
  }
}

结果:

250.compareTo(161)
125.compareTo(161)
187.compareTo(161)
156.compareTo(161)
171.compareTo(161)
163.compareTo(161)
159.compareTo(161)
161.compareTo(161)
161.toString()
161

随机生成一个值,二分找到相应数值。
二分会调用compareTo方法,为代理的Comparable接口中的方法。
System.out.println,会调用对象的toString方法输出到控制台。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 194,390评论 5 459
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 81,821评论 2 371
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 141,632评论 0 319
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,170评论 1 263
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,033评论 4 355
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,098评论 1 272
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,511评论 3 381
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,204评论 0 253
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,479评论 1 290
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,572评论 2 309
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,341评论 1 326
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,213评论 3 312
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,576评论 3 298
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 28,893评论 0 17
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,171评论 1 250
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,486评论 2 341
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 40,676评论 2 335

推荐阅读更多精彩内容

  • 整理来自互联网 1,JDK:Java Development Kit,java的开发和运行环境,java的开发工具...
    Ncompass阅读 1,531评论 0 6
  • Education CodeForces Round 25 题解 Contest 825 Dashboard A....
    染微言阅读 318评论 0 0
  • 叶可可之后就一直和外婆住在乡下,直到高中时才回到D市。家境贫寒的她只好拿奖学金和打工来铮钱交学费。 终于到...
    叶雪阅读 167评论 0 1
  • 到目前为止,我觉得人生遇到的最大的困难是:大学刚毕业的那两年,自我的觉醒的历程。从小到大,我身上最大的标签应该是乖...
    夏希安阅读 166评论 0 2
  • 年少的我们, 总是思考的很多很多。 思考自己所了解的知识, 思考自己现在所从事的工作, 思考我们此时此刻究竟是一个...
    丹华君阅读 166评论 0 0