精华内容
下载资源
问答
  • 方法引用

    千次阅读 2018-05-13 22:32:38
    方法引用在使用 Lambda 表达式的时候,我们实际上传递进去的代码就是一种解决方案: 拿什么参数做什么操作。 ​ 那么考虑一种情况: 如果我们 Lambda 所指定的操作方案,已经有地方存在相同方案,那是否还有...

    方法引用

    在使用 Lambda 表达式的时候,我们实际上传递进去的代码就是一种解决方案:
                                                        拿什么参数做什么操作。
    
    那么考虑一种情况:
        如果我们在 Lambda 中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复的功能代码?

    什么是方法引用

    方法引用就是一个 Lambda 表达式的一种形式,功能与 Lambda 相同。
    
     Java 8 中,我们会使用 Lambda 表达式创建匿名方法。
    
    但是有时候,//Lambda 表达式可能仅仅调用一个已存在的方法,而不做任何其它事,
    
    对于这种情况,通过一个方法名字来引用这个已存在的方法会更加清晰。
    
    //方法引用是一个更加紧凑,易读的 Lambda 表达式。
    
    方法引用的操作符是双冒号"::"

    冗余的 Lambda 场景

     需求说明:1) 创建一个函数式接口 Calculate,包含抽象方法 int calc(int m,int n),用于实现对两个数的计算。2) 创建主类,使用匿名内部类实现 Calculate 接口,并且实现计算的功能。3) 调用 calc()方法,传入参数得到计算结果。4) 使用 Lambda 实例化 Calculate 对象,并且自己写代码实现计算功能。5) 调用 calc()方法,传入参数得到计算结果。

     实现代码:

    /**
    函数式接口
    */
    @FunctionalInterface
    interface Calculate {
     int calc(int m, int n);
    }
    
    public class DemoMethod {
     public static void main(String[] args) {
         
     //使用匿名内部类实现
     Calculate c0 = new Calculate() {
     @Override
     public int calc(int m, int n) {
     return m + n;
     }
     };
         
     System.out.println("计算结果:" + c0.calc(3, 5));
         
     //使用 Lambda 表达式提供解决方案,相当于自己实现这个方法体
     Calculate c1 = (int m, int n) -> m + n;
     System.out.println("计算结果:" + c1.calc(3, 5));
     }
    }

    问题分析

    假设在 Demo01Method 这个类中已经有了计算两个数的静态方法的实现,
    
    我们可以在 Lambda 表达式中直接调用这个方法,
    
    而不需要自己在 Lambda 表达式中去实现这个功能。

     代码步骤:1) 在主类中创建一个静态方法 int sum(int a, int b),实现两个数的相加。2) 使用 Lambda 调用当前类的静态方法 代码实现:

    /**
    函数式接口
    */
    @FunctionalInterface
    interface Calculate {
     int calc(int m, int n);
    }
    
    public class DemoMethod {
     public static void main(String[] args) {
         
     //使用匿名内部类实现
     Calculate c0 = new Calculate() {
     @Override
     public int calc(int m, int n) {
     return m + n;
     }
     };
     System.out.println("计算结果:" + c0.calc(3, 5));
         
     //使用 Lambda 表达式提供解决方案,相当于自己实现这个方法体
     Calculate c1 = (int m, int n) -> m + n;
     System.out.println("计算结果:" + c1.calc(3, 5));
         
     //使用 Lambda 调用当前类的静态方法
     Calculate c2 = (m, n) -> DemoMethod.sum(3, 5);
     System.out.println("计算结果:" + c2.calc(3, 5));
     }
        
     //已经有了实现功能的方法
     private static int sum(int a, int b) {
     return a + b;
     }
    }

    用方法引用改进代码

    能否省去 Lambda 的语法格式(尽管它已经相当简洁)呢?只要“引用”过去就好了:

     案例代码:

    /**
    函数式接口
    */
    @FunctionalInterface
    interface Calculate {
     int calc(int m, int n);
    }
    
    public class DemoMethod {
     public static void main(String[] args) {
         
     //使用匿名内部类实现
     Calculate c0 = new Calculate() {
     @Override
     public int calc(int m, int n) {
     return m + n;
     }
     };
     System.out.println("计算结果:" + c0.calc(3, 5));
         
     //使用 Lambda 表达式提供解决方案,相当于自己实现这个方法体
     Calculate c1 = (int m, int n) -> m + n;
     System.out.println("计算结果:" + c1.calc(3, 5));
         
     //使用 Lambda 调用当前类的静态方法
     Calculate c2 = (m, n) -> DemoMethod.sum(3, 5);
     System.out.println("计算结果:" + c2.calc(3, 5));
         
     //使用方法引用
     Calculate c3 = DemoMethod::sum;
     System.out.println("计算结果:" + c3.calc(3, 5));
     }
        
     //已经有了实现功能的方法
     private static int sum(int a, int b) {
     return a + b;
     }
    }

     请注意其中的双冒号::写法,这被称为“方法引用”,而双冒号是一种新的语法。

    方法引用符

    双冒号::为引用运算符,而它所在的表达式被称为方法引用。
    
    如果 Lambda 要表达的函数方案已经存在于某个方法的实现中,
    那么则可以通过双冒号来引用该方法作为 Lambda 的替代者。
    
     要注意,这里的方法引用功能与 Lambda 是一样的,
    //代替了 Lambda 表达式,也代替了以前的匿名内部类。
    //可以理解为这个方法引用创建了一个匿名内部类,并且实现了接口中的方法。

    语义分析

    对比下面两种写法,完全等效:
    //Lambda 表达式写法 (m, n) -> DemoMethod.sum(3, 5)
    //方法引用写法 DemoMethod::sum
    
     第一种语义是指:拿到参数之后经 Lambda 之手,继而传递给 sum()方法去处理。
     第二种语义是指:直接让 DemoMethod 类来引用 sum()方法来取代 Lambda。
    
    两种写法的执行效果完全一样,而第二种方法引用的写法更加简洁。

    方法引用的过程

    推导与省略

    如果使用 Lambda,那么根据“可推导就是可省略”的原则,
    无需指定参数类型和返回值——它们都将被自动推导。
    
    而如果使用方法引用,也是同样可以根据具体传入的参数值和参数个数进行推导。
    
    //函数式接口是 Lambda 的基础,
    //而方法引用是可以代替 Lambda,让 Lambda 更加简化,但在功能上是一样的。

    方法引用的原则:

    1) 如果 Lambda 表达式的方法体中/*只有一句话*/,而这句话就是调用另一个方法,可以使用方法引用代替。
    
    2) 被引用的方法与函数式接口中的抽象方法://参数类型相同,参数个数相同,返回值类型相同,与方法名无关。
    
     建议被引用的方法与接口中的抽象方法参数类型、返回值类型相同。

    四种方法引用类型

    //静态方法引用 类名::静态方法
    
    //对象方法引用 对象名::成员方法
    
    //类构造器引用 类名::new
    
    //数组构造器引用 类型名[]::new

    类名称引用静态方法的语法

    类名::静态方法(不能使用对象名引用静态方法 
    由于在 java.lang.Math 类中已经存在了静态方法 abs(),用于求一个数的绝对值。
    所以当我们需要通过Lambda 来调用该方法时,有两种写法。
    
    //Lambda 表达式 num -> Math.abs(num)
    
    //方法引用 Math::abs

     案例说明:1) 有一个函数式接口 Calcable,包含抽象方法 int calc(int num)2) 在 Lambda 中调用 Math.abs()方法实现求绝对值3) 直接通过 Math 类方法引用实现求绝对值 实现步骤:1) 创建函数式接口 Calcable,包含抽象方法 int calc(int num),用于计算传入整数,返回计算结果。2) 创建主类,创建主函数,使用 Lambda 表达式创建 Calcable 对象,计算传入整数的绝对值。3) 调用 calc()方法传入-10,输出计算结果4) 使用类方法引用创建 Calcable 对象,直接引用类方法 Math::abs 方法5) 调用 calc()方法传入-10,输出计算结果 实现代码:

    @FunctionalInterface
    interface Calcable {
     int calc(int num);
    }
    public class DemoStaticMethodRef {
     public static void main(String[] args) {
         
     //使用 Lambda 表达式实现
     Calcable c1 = num -> Math.abs(num);
     System.out.println("-10 的绝对值是:" + c1.calc(-10));
         
     //使用类方法引用
     Calcable c2 = Math::abs;
     System.out.println("-10 的绝对值是:" + c2.calc(-10));
     }
    }
    
    //在上面的案例中接口中的 int calc(int num)与被引用的 int Math.abs(int num),
    
    //具有相同的行为,参数类型和返回值类型相同。
    
    //在这个例子中,下面两种写法是等效的:
    
    //下面两种写法是等效的
    
    //Lambda 表达式 num -> Math.abs(num)
    
    //方法引用 Math::abs
    

    通过对象引用成员方法

    对象方法引用又分三种类型:
        //实例上的对象方法引用、父类上的对象方法引用、类型上的对象方法引用
    
    实例上的对象方法引用语法
            对象名::对象方法
            this::本类对象方法  
            super::对象方法

    提问:System.out 是一个对象还是一个类?

    答:查看 System 类的源代码可以得知它是一个 PrintStream 类型的静态成员变量,是一个对象。
    
    所以我们调用System.out.println()其实是调用 out 这个对象的 println()方法。
    
    public final class System {
    public static final PrintStream out = null;
    }

     案例需求:使用 Consumer 接口,调用 accept(字符串),将提供的字符串直接打印出来。 案例步骤:1) 创建主类和主函数2) 创建 Consumer 对象,这是一个函数式接口,有一个抽象方法 void accept(T t)3) 使用 Lambda 表达式,实现方法体,在方法体中调用 System.out.println()方法打印字符串。4) 调用 accept()方法提供要打印的字符串5) 创建 Consumer 对象,使用对象方法引用,引用 out 对象的 println 方法6) 调用 accept()方法提供要打印的字符串 案例代码:

    public class DemoObjMethodRef {
     public static void main(String[] args) {
         
     // 使用 Lambda 表达式,打印字符串
     Consumer c1 = s -> System.out.println(s);
     c1.accept("Hello Java");
         
     //方法引用,打印字符串
     Consumer c2 = System.out::println;
     c2.accept("Hello World");
     }
    }
    //在这个例子中,下面两种写法是等效的:
    
    //下面两种写法是等效的
    
    //Lambda 表达式 s -> System.out.println(s)
    
    //方法引用 System.out::println
    

    类的构造器引用语法:

    类名称::new
    由于构造器的名称与类名完全一样,但一个类可以有多个构造方法,参数不同。

     案例效果: 案例步骤:1) 创建汽车类,有一个 String 属性品牌2) 创建有参的构造方法和无参的构造方法3) 重写 toString()方法,返回 band+"汽车"4) 创建函数式接口 Factory,包含抽象方法 Car makeCar(String name),用于创建汽车对象。5) 创建主类和主函数6) 使用 lambda 表达式直接调用有参的构造方法实例化汽车。7) 调用 makeCar()方法,输出汽车对象。8) 使用构造器引用,调用有参的构造方法,因为接口中的 makeCar 方法是有参数的9) 调用 makeCar()方法,输出汽车对象。 案例代码:

    class Car {
     private String band;
     public Car(String band) {
     this.band = band;
     }
     public Car () {
     }
    
     @Override
     public String toString() {
     return band + "汽车";
     }
    }
    
    @FunctionalInterface
    interface Factory {
     //创建一辆汽车
     Car makeCar(String name);
    }
    public class DemoConstructorRef {
     public static void main(String[] args) {
         
     //使用 lambda 表达式直接实例化汽车返回
     Factory f1 = (name) -> new Car(name);
     Car c1 = f1.makeCar("BMW");
     System.out.println("制造:" + c1);
         
     //使用构造器引用,调用有参的构造方法,因为接口中的 makeCar 方法是有一个参数
     Factory f2 = Car::new;
     Car c2 = f2.makeCar("Audi");
     System.out.println("制造:" + c2);
     }
    }
    // 代码分析:
    //下面两种写法是等效的
    //Lambda 表达式 (name) -> new Car(name)
    //方法引用 Car::new

    数组的构造器引用

    数组构造器语法:
    数组类型[]::new
    数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。
    如果对应到 Lambda 的使用场景中时,需要一个创建数组的函数式接口。

     案例需求:分别使用 Lambda 表达式和数组构造器引用创建 2 个长度各为 5 的整数数组。 案例步骤:1) 创建一个用于创建数组的接口 ArrayBuilder,包含一个抽象方法 int[] buildArray(int length), 提供数组的长度,返回一个创建好的数组。2) 创建主类主函数3) 使用 Lambda 表达式创建上面的接口对象,调用方法创建一个长度为 5 的数组,并且输出数组。4) 使用数组构造器创建上面的接口对象,调用方法创建一个长度为 5 的数组,并且输出数组。 案例代码:

    import java.util.Arrays;
    
    //创建一个用于创建数组的接口
    @FunctionalInterface
    interface ArrayBuilder {
     //提供数组的长度,返回一个创建好的数组
     int[] buildArray(int length);
    }
    
    public class DemoArrayRef {
     public static void main(String[] args) {
         
     //使用 Lambda 表达式创建数组
     ArrayBuilder ab1 = length -> new int[length];
     int [] arr1 = ab1.buildArray(5);
     System.out.println("创建的数组 1:" + Arrays.toString(arr1));
         
     //使用数组构造器创建数组
     ArrayBuilder ab2 = int[]::new;
     int[] arr2 = ab2.buildArray(5);
     System.out.println("创建的数组 2:" + Arrays.toString(arr2));
     }
    }
    // 代码分析:
    //在这个例子中,下面两种写法是等效的:
    //下面两种写法是等效的
    //Lambda 表达式 length -> new int[length]
    //方法引用 int[]::new

    方法引用小结

    方法引用语法

    方法引用类型 语法
    静态方法引用 类名::静态方法
    对象方法引用 对象名::成员方法
    类构造器引用 类名::new
    数组构造器引用 类名[]::new
    
    int calc(int num)  int Math.abs(int x) 
        x -> Math.abs(x)
        Math::abs
        
    void accept(String s) void println(s)
        () -> System.out.println(s) 
        System.out::println
            
    Car makeCar(String name) new Car(String name)
        name-> new Car(name)
        Car::new
                    
    int[] buildArray(int length) new int[length]
        x -> new int[x] 
        int[]::new

    展开全文
  • 编程语言的三大发展阶段:面向机器语言、面向过程语言、面向对象语言 ; 其中面向对象语言主要体现三个特征:封装性、继承、...2、类声明的变量称为对象变量,也简称 对象 ; 3、class 关键字是用来定义类 ; 4、...

    编程语言的三大发展阶段:面向机器语言、面向过程语言、面向对象语言 ;

    其中面向对象语言主要体现三个特征:封装性、继承、动态 ;

    类的定义:类声明和类体。基本格式如下:

    class 类名{

       类体

    }

    特点:

    1、类是Java程序的基本要素,一个Java应用程序就是由若干个类所构成;

    2、类声明的变量称为对象变量,也简称 对象 ;

    3、class 关键字是用来定义类 ;

    4、类的目的是抽象出一类事物共有的属性和行为,并用一定的语法格式来描述所抽象出的属性和行为;

    4、类名必须是合法的Java标识符;

    Java标识符详细说明:(以及中文标识符的说明)

    https://blog.csdn.net/LagerSwan/article/details/104081548


    类的声明:

    如下所示:

    class Number {

    .....

    }

    class 数字 {

    .....

    }

    其中 class Number 与 class 数字 称为类的声明,而 Number 和 数字 则为类名 ;


    类体,即 { } 中的内容:

    类体的内容由如下两部分构成:

    • 变量的声明:用来存储属性的值 ;(体现了对象的属性)
    • 方法的定义:方法可以对类中声明的变量进行操作;(体现了对象的行为)

    如下代码演示:

    public class Class_Test {
    	int number = 10;
    	double numberd = 11.111;
    	double TestDouble() {
    		double numbers = 100 ;
    		numbers += numberd;
    		return numbers;
    	}
    	public static void main(String[] args) {
    		Class_Test test = new Class_Test();
    		System.out.println(test.TestDouble());
    	}
    }

    在以上代码中,声明了一个类 Class_Test ,两个变量 number 与 numberd ,一个方法 TestDouble() ,一个主方法 main () ;


    成员变量的定义:

    一、成员变量的类型:

    成员变量的类型可以是Java中的任何一种数据类型,包括了基本数据类型:整形、浮点型、逻辑类型、字符类型 ;引用类型中的:数组、对象和接口;  

    如下代码演示:

    public class Class_Test {
    
    	public static void main(String[] args) {
    		
    		//成员变量的说明
    		//基本数据类型的成员变量
    		int testt = 11 ;
    		float testf = 11.111f ;
    		double testd = 11.11111 ;
    		//引用类型的变量
    		int a [] ;
    		Class_Test test ;
    		
    	}
    }

    在以上代码中,成员变量:testt,testf,testd 是基本数据类型的变量;a [ ] ,test是引用类型的变量,a [ ] 为数组的变量,test 为类声明的变量,即类对象;

    二、成员变量的有效范围:

    成员变量在整个类内都是有效的,其有效性与它在类体中声明的先后位置是不关的;

    如下代码演示:   最终输出: 110

    public class Class_Test {
    	
    	//成员变量的有效范围:
    	int number = 11 ;
    	int Int_Test(){
    		int int1 = number * digit ;
    		return int1;
    	}
    	int digit = 10 ;
    
    	public static void main(String[] args) {
    		//类体的说明
    		Class_Test test = new Class_Test();
    		System.out.println(test.Int_Test());	
    	}
    }
    

    不建议大家这样编写程序,当代码量多时,对代码的可读性有一定的影响,建议先声明成员变量,再定义方法;

    三、成员变量的编写风格:

    建议大家在声明成员变量时,变量的名字使用驼峰规则,即变量名由多个单词组成时,从第二个单词开始的其他单词的首字母大学;如:computerArea 、ClassExceptionShow 


    方法的使用:

    在类体中,方法的定义包括两部分:方法头、方法体 ;  一般格式如下:

    方法头 {

       方法体

    }

    一、方法头说明

    方法头由方法的类型、名称、名称后面的 () 以及 ()里面的参数列表所构成;

    如下代码演示:

    //无参数的方法头
    double testd (){
        return 11.111;
    }
    
    //有参数的方法头, x 和 y 即为参数
    double testd (double x,double y){
        return x+y;
    }

    二、方法体说明:

    方法体即 方法()后面的 { } 以及 { } 里面的内容;

    在方法体中声明的变量为局部变量,而不是成员变量,局部变量只在方法体内有效,而且与声明时的前后位置有关;

    如下代码演示:

    double testd = 10 ;
    double testd (double x,double y){      //参数 x,y是局部变量
       double sum;                         //参数 sum 是局部变量
       sum = x + y ;
    
       if(sum>x){
           sum += testd ;         //参数 testd 是成员变量(全局变量)
    }
    
        return sum;        //return 语句,返回 sum 的值
    }

    局部变量的介绍:

    1、局部变量不同与成员变量,局部变量只在方法中有效,而且与其声明的位置顺序有关;

    2、若局部变量声明在一个复合语句中,那么该局部变量的有效范围只在复合语句中有效;

    double testd (double x,double y){      //参数 x,y是局部变量
    	   double sum;                         //参数 sum 是局部变量
    	   sum = x + y ;
    	   if(sum != 0){
    	       int ints = 1;        // ints 为复合语句中的局部变量
    	       sum += ints ;
    	}
            double bottom = 1.1;
            bottom += ints ;         //程序报错:ints cannot be resolved to a variable
    	    return sum;
    	}

    3、若局部变量声明在一个循环语句中,那么该局部变量的有效范围只在循环语句中有效;和以上复合语句是一样的;


    总结:成员变量与局部变量的区别

    1、局部变量不同与成员变量,局部变量只在方法中有效,而成员变量在整个类中都有效;

    2、局部变量与其声明的位置顺序有关,而成员变量与其声明的位置顺序无关;

    3、若局部变量的名字和成员变量的名字相同,那么成员变量会被隐藏,即在方法体中成员变量暂时是无效的,如下代码演示:

    class Test_Variable {
        int x = 10 , y ;
       void tests () {
          int x = 5 ;
          y = x + 10 ; //y的值为 15 ,而不是20,此时成员变量 x = 10 在该方法体中,暂时失效;
       }
    
    }

    4、当想使用方法中被隐藏的成员变量,可以使用关键字 this 进行调用,如下代码演示:

    class Test_Variable {
        int x = 10 , y ;
       void tests () {
          int x = 5 ;
          y = this.x + 10 ; //此时 y 的值为 20 ,而不是15,this.x调用的为成员变量 x = 10 ;
       }
    
    }

    5、成员变量有默认值,而局部变量没有默认值,因此在使用局部变量时,要确保该局部变量是有初始值的,否则程序报错;

    class Test_Variable {
    
               int  y ;
    	   void tests () {
    	      int x;       //报错:The local variable x may not have been initialized
    	      y = x + 10 ; 
    	   }
    
    }

     

     

     

    展开全文
  • Java值传递和引用传递详细说明

    千次阅读 多人点赞 2020-07-14 15:53:17
    学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运用却解释不出原理,而且坊间讨论的话题又是充满争议:有的论坛帖子说Java只有值传递,...

    本文旨在用最通俗的语言讲述最枯燥的基本知识

    学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运用却解释不出原理,而且坊间讨论的话题又是充满争议:有的论坛帖子说Java只有值传递,有的博客说两者皆有;这让人有点摸不着头脑,下面我们就这个话题做一些探讨,对书籍、对论坛博客的说法,做一次考证,以得出信得过的答案。

    其实,对于值传递和引用传递的语法和运用,百度一下,就能出来可观的解释和例子数目,或许你看一下例子好像就懂,但是当你参加面试,做一道这个知识点的笔试题时感觉自己会,胸有成熟的写了答案,却发现是错的,或者是你根本不会做。

    是什么原因?

    那是因为你对知识点没有了解透彻,只知道其皮毛。要熟读一个语法很简单,要理解一行代码也不难,但是能把学过的知识融会贯通,串联起来理解,那就是非常难了,在此,关于值传递和引用传递,小编会从以前学过的基础知识开始,从内存模型开始,一步步的引出值传递和引用传递的本质原理,故篇幅较长,知识点较多,望读者多有包涵。

    1. 形参与实参

    我们先来重温一组语法:

    1. 形参:方法被调用时需要传递进来的参数,如:func(int a)中的a,它只有在func被调用期间a才有意义,也就是会被分配内存空间,在方法func执行完成后,a就会被销毁释放空间,也就是不存在了

    2. 实参:方法被调用时是传入的实际值,它在方法被调用前就已经被初始化并且在方法被调用时传入。

    举个栗子:

    1public static void func(int a){
    2 a=20;
    3 System.out.println(a);
    4}
    5public static void main(String[] args) {
    6 int a=10;//变量
    7 func(a);
    8}
    

    例子中
    int a=10;中的a在被调用之前就已经创建并初始化,在调用func方法时,他被当做参数传入,所以这个a是实参。
    而func(int a)中的a只有在func被调用时它的生命周期才开始,而在func调用结束之后,它也随之被JVM释放掉,,所以这个a是形参。

    2. Java的数据类型

    所谓数据类型,是编程语言中对内存的一种抽象表达方式,我们知道程序是由代码文件和静态资源组成,在程序被运行前,这些代码存在在硬盘里,程序开始运行,这些代码会被转成计算机能识别的内容放到内存中被执行。
    因此

    数据类型实质上是用来定义编程语言中相同类型的数据的存储形式,也就是决定了如何将代表这些值的位存储到计算机的内存中。

    所以,数据在内存中的存储,是根据数据类型来划定存储形式和存储位置的。
    那么
    Java的数据类型有哪些?

    1. 基本类型:编程语言中内置的最小粒度的数据类型。它包括四大类八种类型:

    4种整数类型:byte、short、int、long
    2种浮点数类型:float、double
    1种字符类型:char
    1种布尔类型:boolean

    1. 引用类型:引用也叫句柄,引用类型,是编程语言中定义的在句柄中存放着实际内容所在地址的地址值的一种数据形式。它主要包括:


    接口
    数组

    有了数据类型,JVM对程序数据的管理就规范化了,不同的数据类型,它的存储形式和位置是不一样的,要想知道JVM是怎么存储各种类型的数据,就得先了解JVM的内存划分以及每部分的职能。

    3.JVM内存的划分及职能

    Java语言本身是不能操作内存的,它的一切都是交给JVM来管理和控制的,因此Java内存区域的划分也就是JVM的区域划分,在说JVM的内存划分之前,我们先来看一下Java程序的执行过程,如下图:

    640?wx_fmt=png


    有图可以看出:Java代码被编译器编译成字节码之后,JVM开辟一片内存空间(也叫运行时数据区),通过类加载器加到到运行时数据区来存储程序执行期间需要用到的数据和相关信息,在这个数据区中,它由以下几部分组成:

     

    1. 虚拟机栈

    2. 堆

    3. 程序计数器

    4. 方法区

    5. 本地方法栈

    我们接着来了解一下每部分的原理以及具体用来存储程序执行过程中的哪些数据。


    1. 虚拟机栈

    虚拟机栈是Java方法执行的内存模型,栈中存放着栈帧,每个栈帧分别对应一个被调用的方法,方法的调用过程对应栈帧在虚拟机中入栈到出栈的过程。

    栈是线程私有的,也就是线程之间的栈是隔离的;当程序中某个线程开始执行一个方法时就会相应的创建一个栈帧并且入栈(位于栈顶),在方法结束后,栈帧出栈。

    下图表示了一个Java栈的模型以及栈帧的组成:

    640?wx_fmt=png


    栈帧:是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。

     

    每个栈帧中包括:

    1. 局部变量表:用来存储方法中的局部变量(非静态变量、函数形参)。当变量为基本数据类型时,直接存储值,当变量为引用类型时,存储的是指向具体对象的引用。

    2. 操作数栈:Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指操作数栈。

    3. 指向运行时常量池的引用:存储程序执行时可能用到常量的引用。

    4. 方法返回地址:存储方法执行完成后的返回地址。


    2. 堆:

    堆是用来存储对象本身和数组的,在JVM中只有一个堆,因此,堆是被所有线程共享的。


    3. 方法区:

    方法区是一块所有线程共享的内存逻辑区域,在JVM中只有一个方法区,用来存储一些线程可共享的内容,它是线程安全的,多个线程同时访问方法区中同一个内容时,只能有一个线程装载该数据,其它线程只能等待。

    方法区可存储的内容有:类的全路径名、类的直接超类的权全限定名、类的访问修饰符、类的类型(类或接口)、类的直接接口全限定名的有序列表、常量池(字段,方法信息,静态变量,类型引用(class))等。


    4. 本地方法栈:

    本地方法栈的功能和虚拟机栈是基本一致的,并且也是线程私有的,它们的区别在于虚拟机栈是为执行Java方法服务的,而本地方法栈是为执行本地方法服务的。

    有人会疑惑:什么是本地方法?为什么Java还要调用本地方法?


    5. 程序计数器:

    线程私有的。
    记录着当前线程所执行的字节码的行号指示器,在程序运行过程中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。


    4. 数据如何在内存中存储?

    从上面程序运行图我们可以看到,JVM在程序运行时的内存分配有三个地方:

    • 静态方法区

    • 常量区

    相应地,每个存储区域都有自己的内存分配策略:

    • 堆式:

    • 栈式

    • 静态

    我们已经知道:Java中的数据类型有基本数据类型和引用数据类型,那么这些数据的存储都使用哪一种策略呢?
    这里要分以下的情况进行探究:

    1. 基本数据类型的存储:

    • A. 基本数据类型的局部变量

    • B. 基本数据类型的成员变量

    • C. 基本数据类型的静态变量

    2. 引用数据类型的存储


    1. 基本数据类型的存储


    我们分别来研究一下:

    A.基本数据类型的局部变量

    1. 定义基本数据类型的局部变量以及数据都是直接存储在内存中的栈上,也就是前面说到的“虚拟机栈”,数据本身的值就是存储在栈空间里面。

      640?wx_fmt=png


      如上图,在方法内定义的变量直接存储在栈中,如

    1int age=50;
    2int weight=50;
    3int grade=6;
    

    当我们写“int age=50;”,其实是分为两步的:

    1int age;//定义变量
    2age=50;//赋值
    

    首先JVM创建一个名为age的变量,存于局部变量表中,然后去栈中查找是否存在有字面量值为50的内容,如果有就直接把age指向这个地址,如果没有,JVM会在栈中开辟一块空间来存储“50”这个内容,并且把age指向这个地址。因此我们可以知道:
    我们声明并初始化基本数据类型的局部变量时,变量名以及字面量值都是存储在栈中,而且是真实的内容。

    我们再来看“int weight=50;”,按照刚才的思路:字面量为50的内容在栈中已经存在,因此weight是直接指向这个地址的。由此可见:栈中的数据在当前线程下是共享的

    那么如果再执行下面的代码呢?

    1weight=40;
    

    当代码中重新给weight变量进行赋值时,JVM会去栈中寻找字面量为40的内容,发现没有,就会开辟一块内存空间存储40这个内容,并且把weight指向这个地址。由此可知:

    基本数据类型的数据本身是不会改变的,当局部变量重新赋值时,并不是在内存中改变字面量内容,而是重新在栈中寻找已存在的相同的数据,若栈中不存在,则重新开辟内存存新数据,并且把要重新赋值的局部变量的引用指向新数据所在地址。


    B. 基本数据类型的成员变量

    成员变量:顾名思义,就是在类体中定义的变量。
    看下图:

    640?wx_fmt=png

     

    我们看per的地址指向的是堆内存中的一块区域,我们来还原一下代码:

     1public class Person{
     2  private int age;
     3  private String name;
     4  private int grade;
     5//篇幅较长,省略setter getter方法
     6  static void run(){
     7     System.out.println("run...."); 
     8   };
     9}
    10
    11//调用
    12Person per=new Person();
    

    同样是局部变量的age、name、grade却被存储到了堆中为per对象开辟的一块空间中。因此可知:基本数据类型的成员变量名和值都存储于堆中,其生命周期和对象的是一致的。


    C. 基本数据类型的静态变量

    前面提到方法区用来存储一些共享数据,因此基本数据类型的静态变量名以及值存储于方法区的运行时常量池中,静态变量随类加载而加载,随类消失而消失


    2. 引用数据类型的存储:

    上面提到:堆是用来存储对象本身和数组,而引用(句柄)存放的是实际内容的地址值,因此通过上面的程序运行图,也可以看出,当我们定义一个对象时

    1Person per=new Person();
    

    实际上,它也是有两个过程:

    1Person per;//定义变量
    2per=new Person();//赋值
    

    在执行Person per;时,JVM先在虚拟机栈中的变量表中开辟一块内存存放per变量,在执行per=new Person()时,JVM会创建一个Person类的实例对象并在堆中开辟一块内存存储这个实例,同时把实例的地址值赋值给per变量。因此可见:
    对于引用数据类型的对象/数组,变量名存在栈中,变量值存储的是对象的地址,并不是对象的实际内容。

    6. 值传递和引用传递

    前面已经介绍过形参和实参,也介绍了数据类型以及数据在内存中的存储形式,接下来,就是文章的主题:值传递和引用的传递。

    值传递:
    在方法被调用时,实参通过形参把它的内容副本传入方法内部,此时形参接收到的内容是实参值的一个拷贝,因此在方法内对形参的任何操作,都仅仅是对这个副本的操作,不影响原始值的内容。

    来看个例子:

     1public static void valueCrossTest(int age,float weight){
     2    System.out.println("传入的age:"+age);
     3    System.out.println("传入的weight:"+weight);
     4    age=33;
     5    weight=89.5f;
     6    System.out.println("方法内重新赋值后的age:"+age);
     7    System.out.println("方法内重新赋值后的weight:"+weight);
     8    }
     9
    10//测试
    11public static void main(String[] args) {
    12        int a=25;
    13        float w=77.5f;
    14        valueCrossTest(a,w);
    15        System.out.println("方法执行后的age:"+a);
    16        System.out.println("方法执行后的weight:"+w);
    17}
    

    输出结果:

    1传入的age:25
    2传入的weight:77.5
    3
    4方法内重新赋值后的age:33
    5方法内重新赋值后的weight:89.5
    6
    7方法执行后的age:25
    8方法执行后的weight:77.5
    

    从上面的打印结果可以看到:
    a和w作为实参传入valueCrossTest之后,无论在方法内做了什么操作,最终a和w都没变化。

    这是什么造型呢?!!

    下面我们根据上面学到的知识点,进行详细的分析:

    首先程序运行时,调用mian()方法,此时JVM为main()方法往虚拟机栈中压入一个栈帧,即为当前栈帧,用来存放main()中的局部变量表(包括参数)、操作栈、方法出口等信息,如a和w都是mian()方法中的局部变量,因此可以断定,a和w是躺着mian方法所在的栈帧中
    如图:

    640?wx_fmt=jpeg


    而当执行到valueCrossTest()方法时,JVM也为其往虚拟机栈中压入一个栈,即为当前栈帧,用来存放valueCrossTest()中的局部变量等信息,因此age和weight是躺着valueCrossTest方法所在的栈帧中,而他们的值是从a和w的值copy了一份副本而得,如图:

    640?wx_fmt=png

    因而可以a和age、w和weight对应的内容是不一致的,所以当在方法内重新赋值时,实际流程如图:

    640?wx_fmt=jpeg

    也就是说,age和weight的改动,只是改变了当前栈帧(valueCrossTest方法所在栈帧)里的内容,当方法执行结束之后,这些局部变量都会被销毁,mian方法所在栈帧重新回到栈顶,成为当前栈帧,再次输出a和w时,依然是初始化时的内容。
    因此:
    值传递传递的是真实内容的一个副本,对副本的操作不影响原内容,也就是形参怎么变化,不会影响实参对应的内容。

     

    引用传递:
    ”引用”也就是指向真实内容的地址值,在方法调用时,实参的地址通过方法调用被传递给相应的形参,在方法体内,形参和实参指向通愉快内存地址,对形参的操作会影响的真实内容。

    举个栗子:
    先定义一个对象:

     1public class Person {
     2        private String name;
     3        private int age;
     4
     5        public String getName() {
     6            return name;
     7        }
     8        public void setName(String name) {
     9            this.name = name;
    10        }
    11        public int getAge() {
    12            return age;
    13        }
    14        public void setAge(int age) {
    15            this.age = age;
    16        }
    17}
    

    我们写个函数测试一下:

     1public static void PersonCrossTest(Person person){
     2        System.out.println("传入的person的name:"+person.getName());
     3        person.setName("我是张小龙");
     4        System.out.println("方法内重新赋值后的name:"+person.getName());
     5    }
     6//测试
     7public static void main(String[] args) {
     8        Person p=new Person();
     9        p.setName("我是马化腾");
    10        p.setAge(45);
    11        PersonCrossTest(p);
    12        System.out.println("方法执行后的name:"+p.getName());
    13}
    

    输出结果:

    1传入的person的name:我是马化腾
    2方法内重新赋值后的name:我是张小龙
    3方法执行后的name:我是张小龙
    

    可以看出,person经过personCrossTest()方法的执行之后,内容发生了改变,这印证了上面所说的“引用传递”,对形参的操作,改变了实际对象的内容。

    那么,到这里就结题了吗?
    不是的,没那么简单,
    能看得到想要的效果
    是因为刚好选对了例子而已!!!

    下面我们对上面的例子稍作修改,加上一行代码,

    1public static void PersonCrossTest(Person person){
    2        System.out.println("传入的person的name:"+person.getName());
    3        person=new Person();//加多此行代码
    4        person.setName("我是张小龙");
    5        System.out.println("方法内重新赋值后的name:"+person.getName());
    6    }
    

    输出结果:

    1传入的person的name:我是马化腾
    2方法内重新赋值后的name:我是张小龙
    3方法执行后的name:我是马化腾
    

    `
    为什么这次的输出和上次的不一样了呢?
    看出什么问题了吗?

    按照上面讲到JVM内存模型可以知道,对象和数组是存储在Java堆区的,而且堆区是共享的,因此程序执行到main()方法中的下列代码时

    1Person p=new Person();
    2        p.setName("我是马化腾");
    3        p.setAge(45);
    4        PersonCrossTest(p);
    

    JVM会在堆内开辟一块内存,用来存储p对象的所有内容,同时在main()方法所在线程的栈区中创建一个引用p存储堆区中p对象的真实地址,如图:

    640?wx_fmt=png


    当执行到PersonCrossTest()方法时,因为方法内有这么一行代码:

    1person=new Person();
    

    JVM需要在堆内另外开辟一块内存来存储new Person(),假如地址为“xo3333”,那此时形参person指向了这个地址,假如真的是引用传递,那么由上面讲到:引用传递中形参实参指向同一个对象,形参的操作会改变实参对象的改变

    可以推出:实参也应该指向了新创建的person对象的地址,所以在执行PersonCrossTest()结束之后,最终输出的应该是后面创建的对象内容。

    然而实际上,最终的输出结果却跟我们推测的不一样,最终输出的仍然是一开始创建的对象的内容。

    由此可见:引用传递,在Java中并不存在。

    但是有人会疑问:为什么第一个例子中,在方法内修改了形参的内容,会导致原始对象的内容发生改变呢?

    这是因为:无论是基本类型和是引用类型,在实参传入形参时,都是值传递,也就是说传递的都是一个副本,而不是内容本身。

    640?wx_fmt=png

     

    有图可以看出,方法内的形参person和实参p并无实质关联,它只是由p处copy了一份指向对象的地址,此时:

    p和person都是指向同一个对象

    因此在第一个例子中,对形参p的操作,会影响到实参对应的对象内容。而在第二个例子中,当执行到new Person()之后,JVM在堆内开辟一块空间存储新对象,并且把person改成指向新对象的地址,此时:

    p依旧是指向旧的对象,person指向新对象的地址。

    所以此时对person的操作,实际上是对新对象的操作,于实参p中对应的对象毫无关系

    结语

    因此可见:在Java中所有的参数传递,不管基本类型还是引用类型,都是值传递,或者说是副本传递。
    只是在传递过程中:

    如果是对基本数据类型的数据进行操作,由于原始内容和副本都是存储实际值,并且是在不同的栈区,因此形参的操作,不影响原始内容。

    如果是对引用类型的数据进行操作,分两种情况,一种是形参和实参保持指向同一个对象地址,则形参的操作,会影响实参指向的对象的内容。一种是形参被改动指向新的对象地址(如重新赋值引用),则形参的操作,不会影响实参指向的对象的内容。

    原文转至:https://blog.csdn.net/bntx2jsqfehy7/article/details/83508006

    展开全文
  • Java8新特性之Stream流以及方法引用

    千次阅读 2019-03-09 21:33:39
    Java8新特性之Stream流以及方法引用一. String流1. 引言2. 流式思想概述3. 获取流4. 常用方法4.1 逐一处理:forEach4.2 过滤:filter4.3 映射:map4.4 统计个数:count4.5 取前几个:limit4.6 跳过前几个:skip4.7 ...

    一. String流

    说到Stream便容易想到I/O Stream,而实际上,谁规定“流”就一定是“IO流”呢?在Java 8中,得益于Lambda所带来的函数式编程,引入了一个全新的Stream概念,用于解决已有集合类库既有的弊端。

    1. 引言

    传统集合的多步遍历代码:
    几乎所有的集合(如 Collection 接口或 Map 接口等)都支持直接或间接的遍历操作。而当我们需要对集合中的元素进行操作的时候,除了必需的添加、删除、获取外,最典型的就是集合遍历。例如:

    package Demo02;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class Demo01ForEach {
        public static void main(String[] args){
            List<String> list = new ArrayList<>();
            list.add("张无忌");
            list.add("周芷若");
            list.add("赵敏");
            list.add("张强");
            list.add("张三丰");
    
            for(String s : list){
                System.out.println(s);
            }
        }
    }
    
    

    运行结果如下:

    这是一段非常简单的集合遍历操作:对集合中的每一个字符串都进行打印输出操作。

    循环遍历的弊端:
    Java 8的Lambda表达式让我们可以更加专注于做什么(What),而不是怎么做(How),这点此前已经结合内部类进行了对比说明。现在,我们仔细体会一下上例代码,可以发现:

    1. for循环的语法就是“怎么做”。
    2. for循环的循环体才是“做什么”.

    为什么使用循环?因为要进行遍历。但循环是遍历的唯一方式吗?遍历是指每一个元素逐一进行处理,而并不是从第一个到最后一个顺次处理的循环。前者是目的,后者是方式。
    试想一下,如果希望对集合中的元素进行筛选过滤:

    1. 将集合A根据条件一过滤为子集B;
    2. 然后再根据条件二过滤为子集C。

    那怎么办?在Java 8之前的做法可能为:

    package Demo02;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class Demo02NormalFilter {
        public static void main(String[] args){
            List<String> listA = new ArrayList<>();
            listA.add("张无忌");
            listA.add("周芷若");
            listA.add("赵敏");
            listA.add("张强");
            listA.add("张三丰");
    
            List<String> listB = new ArrayList<>();
            for(String s : listA){
                if(s.startsWith("张")){
                    listB.add(s);
                }
            }
    
            List<String> listC = new ArrayList<>();
            for(String s : listB){
                if(s.length() == 3){
                    listC.add(s);
                }
            }
    
            for(String s : listC){
                System.out.println(s);
            }
        }
    }
    

    运行结果如下:

    这段代码中含有三个循环,每一个作用不同:

    1. 首先筛选所有姓张的人;
    2. 然后筛选名字有三个字的人;
    3. 最后进行对结果进行打印输出。

    每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循每当我们需要对集合中的元素进行操作的时候,总是需要进行循环、循环、再循环。这是理所当然的么?不是。循环是做事情的方式,而不是目的。另一方面,使用线性循环就意味着只能遍历一次。如果希望再次遍历,只能再使用另一个循环从头开始。
    那么Lambda的衍生物Stream能给我们带来怎样更加优雅的写法呢?

    Stream的更优写法:
    下面来看一下借助Java 8的Stream API,什么才叫优雅:

    package Demo02;
    
    import java.util.ArrayList;
    import java.util.List;
    
    public class Demo03StreamFilter {
        public static void main(String[] args){
            List<String> list = new ArrayList<>();
            list.add("张无忌");
            list.add("周芷若");
            list.add("赵敏");
            list.add("张强");
            list.add("张三丰");
    
            list.stream()
                    .filter(s->s.startsWith("张"))
                    .filter(s->s.length() == 3)
                    .forEach(System.out::println);
        }
    }
    

    直接阅读代码的字面意思即可完美展示无关逻辑方式的语义:获取流、过滤姓张、过滤长度为3、逐一打印。代码中并没有体现使用线性循环或是其他任何算法进行遍历,我们真正要做的事情内容被更好地体现在代码中。

    2. 流式思想概述

    注意:请暂时忘记对传统IO流的固有印象!

    Stream API可以极大提高Java程序员的生产力,让程序员写出高效率、干净、简洁的代码。
    这种风格将要处理的元素集合看作一种流, 流在管道中传输, 并且可以在管道的节点上进行处理, 比如筛选, 排序,聚合等。
    元素流在管道中经过中间操作(intermediate operation)的处理,最后由最终操作(terminal operation)得到前面处理的结果。
    整体来看,流式思想类似于工厂车间的“生产流水线”。

    这里的 filter 、 map 、 skip 都是在对函数模型进行操作,集合元素并没有真正被处理。只有当终结方法 count执行的时候,整个模型才会按照指定策略执行操作。而这得益于Lambda的延迟执行特性。

    备注:“Stream流”其实是一个集合元素的函数模型,它并不是集合,也不是数据结构,其本身并不存储任何元素(或其地址值)。

    什么是Stream?

    Stream(流)是一个来自数据源的元素队列并支持聚合操作

    1. 元素是特定类型的对象,形成一个队列。 Java中的Stream并不会存储元素,而是按需计算。
    2. 数据源:流的来源。 可以是集合,数组,I/O channel, 产生器generator 等。
    3. 聚合操作:类似SQL语句一样的操作, 比如filter, map, reduce, find, match, sorted等。

    和以前的Collection操作不同, Stream操作还有两个基础的特征:

    1. Pipelining: 中间操作都会返回流对象本身。 这样多个操作可以串联成一个管道, 如同流式风格(fluent style)。 这样做可以对操作进行优化, 比如延迟执行(laziness)和短路( short-circuiting)。
    2. 内部迭代: 以前对集合遍历都是通过Iterator或者For-Each的方式, 显式的在集合外部进行迭代, 这叫做外部迭代。 Stream提供了内部迭代的方式, 通过访问者模式(Visitor)实现。

    当使用一个流的时候,通常包括三个基本步骤:获取一个数据源(source)→ 数据转换→执行操作获取想要的结果,每次转换原有 Stream 对象不改变,返回一个新的 Stream 对象(可以有多次转换),这就允许对其操作可以像链条一样排列,变成一个管道。

    3. 获取流

    java.util.stream.Stream 是Java 8新加入的最常用的流接口。(这并不是一个函数式接口。)
    获取一个流非常简单,有以下几种常用的方式:

    1. 所有的 Collection 集合都可以通过 stream 默认方法获取流;
    2. Stream 接口的静态方法 of 可以获取数组对应的流。

    根据Collection获取流:

    首先, java.util.Collection 接口中加入了default方法 stream 用来获取流,所以其所有实现类均可获取流.

    package Demo02;
    
    import java.util.*;
    import java.util.stream.Stream;
    
    public class Demo04Collection {
        public static void main(String[] args) {
            List<String> list = new ArrayList<>();
            Stream<String> stream1 = list.stream();
            
            Set<String> set = new HashSet<>();
            Stream<String> stream2 = set.stream();
            
            Vector<String> vector = new Vector<>();
            Stream<String> stream3 = vector.stream();
        }
    }
    

    根据Map获取流:

    java.util.Map 接口不是 Collection 的子接口,且其K-V数据结构不符合流元素的单一特征,所以获取对应的流需要分key、value或entry等情况:

    package Demo02;
    
    import java.util.HashMap;
    import java.util.Map;
    import java.util.stream.Stream;
    
    public class Demo05Map {
        public static void main(String[] args){
            Map<String,String> map = new HashMap<>();
            Stream<String> keyStream = map.keySet().stream();
            Stream<String> valueStream = map.values().stream();
            Stream<Map.Entry<String,String>> entrys = map.entrySet().stream();
        }
    }
    

    根据数组获取流

    如果使用的不是集合或映射而是数组,由于数组对象不可能添加默认方法,所以 Stream 接口中提供了静态方法of ,使用很简单:

    package Demo02;
    
    import java.util.stream.Stream;
    
    public class Demo06GetStream {
        public static void main(String[] args){
            String[] array = {"私","忆","一","秒","钟"};
            Stream<String> stream = Stream.of(array);
        }
    }
    

    备注: of 方法的参数其实是一个可变参数,所以支持数组

    4. 常用方法

    流模型的操作很丰富,这里介绍一些常用的API。这些方法可以被分成两种:

    1. 延迟方法:返回值类型仍然是 Stream 接口自身类型的方法,因此支持链式调用。(除了终结方法外,其余方法均为延迟方法。)
    2. 终结方法:返回值类型不再是 Stream 接口自身类型的方法,因此不再支持类似 StringBuilder 那样的链式调用。本小节中,终结方法包括 count 和 forEach 方法。

    4.1 逐一处理:forEach

    虽然方法名字叫 forEach ,但是与for循环中的“for-each”昵称不同。

    void forEach(Consumer<? super T> action);
    

    该方法接收一个 Consumer 接口函数,会将每一个流元素交给该函数进行处理。

    Consumer接口:

    java.util.function.Consumer<T>接口是一个消费型接口。
    Consumer接口中包含抽象方法void accept(T t),意为消费一个指定泛型的数据。
    

    基本使用:

    package Demo02;
    
    import java.util.stream.Stream;
    
    public class Demo07Consumer {
        public static void main(String[] args){
            Stream<String> stream = Stream.of("私","忆","一","秒","钟");
            stream.forEach(s->System.out.println(s));
        }
    }
    

    运行结果如下:

    4.2 过滤:filter

    可以通过 filter 方法将一个流转换成另一个子集流。

    Stream<T> filter(Predicate<? super T> predicate);
    

    该接口接收一个 Predicate 函数式接口参数(可以是一个Lambda或方法引用)作为筛选条件。

    Predicate接口:
    java.util.stream.Predicate 函数式接口,其中唯一的抽象方法为:

    boolean test(T t);
    

    该方法将会产生一个boolean值结果,代表指定的条件是否满足。如果结果为true,那么Stream流的 filter 方法
    将会留用元素;如果结果为false,那么 filter 方法将会舍弃元素。

    基本使用:

    package Demo02;
    
    import java.util.stream.Stream;
    
    public class Demo08StreamFilter {
        public static void main(String[] args){
            Stream<String> original = Stream.of("张无忌","张三丰","周芷若");
            Stream<String> result = original.filter(s->s.startsWith("张"));
        }
    }
    

    在这里通过Lambda表达式来指定了筛选的条件:必须姓张。

    4.3 映射:map

    如果需要将流中的元素映射到另一个流中,可以使用 map 方法。方法签名:

    <R> Stream<R> map(Function<? super T, ? extends R> mapper);
    

    该接口需要一个 Function 函数式接口参数,可以将当前流中的T类型数据转换为另一种R类型的流。

    Function接口:
    java.util.stream.Function 函数式接口,其中唯一的抽象方法为:

    R apply(T t);
    

    这可以将一种T类型转换成为R类型,而这种转换的动作,就称为“映射”。

    基本使用:

    ackage Demo02;
    
    import java.util.stream.Stream;
    
    public class Demo09StreamMap {
        public static void main(String[] args){
            Stream<String> original = Stream.of("1","2","3");
            Stream<Integer> result = original.map(s->Integer.parseInt(s));
        }
    }
    

    这段代码中, map 方法的参数通过方法引用,将字符串类型转换成为了int类型(并自动装箱为 Integer 类对象)。

    4.4 统计个数:count

    正如旧集合 Collection 当中的 size 方法一样,流提供 count 方法来数一数其中的元素个数:

    long count();
    

    该方法返回一个long值代表元素个数(不再像旧集合那样是int值)。
    基本使用:

    package Demo02;
    
    import java.util.stream.Stream;
    
    public class Demo10StreamCount {
        public static void main(String[] args){
            Stream<String> original = Stream.of("张无忌", "张三丰", "周芷若");
            Stream<String> result = original.filter(s->s.startsWith("张"));
            System.out.println(result.count());
        }
    }
    

    运行结果如下:

    4.5 取前几个:limit

    limit 方法可以对流进行截取,只取用前n个。方法签名:

    Stream<T> limit(long maxSize);
    

    参数是一个long型,如果集合当前长度大于参数则进行截取;否则不进行操作。
    基本使用:

    package Demo02;
    
    import java.util.stream.Stream;
    
    public class Demo11StreamLimit {
        public static void main(String[] args){
            Stream<String> stream = Stream.of("私","忆","一","秒","钟");
            Stream<String> stream2 = stream.limit(2);
            System.out.println(stream2.count());
        }
    }
    

    运行结果如下:

    4.6 跳过前几个:skip

    如果希望跳过前几个元素,可以使用 skip 方法获取一个截取之后的新流:

    Stream<T> skip(long n);
    

    如果流的当前长度大于n,则跳过前n个;否则将会得到一个长度为0的空流。
    基本使用:

    package Demo02;
    
    import java.util.stream.Stream;
    
    public class Demo12StreamSkip {
        public static void main(String[] args){
            Stream<String> stream = Stream.of("私","忆","一","秒","钟");
            Stream<String> stream2 = stream.skip(2);
            System.out.println(stream2.count());
        }
    }
    

    运行结果如下:

    4.7 组合:concat

    如果有两个流,希望合并成为一个流,那么可以使用 Stream 接口的静态方法 concat :

    static <T> Stream<T> concat(Stream<? extends T> a, Stream<? extends T> b)
    

    备注:这是一个静态方法,与 java.lang.String 当中的 concat 方法是不同的

    该方法的基本使用代码如:

    package Demo02;
    
    import java.util.stream.Stream;
    
    public class Demo13StreamConcat {
        public static void main(String[] args){
            Stream<String> stream = Stream.of("私","忆");
            Stream<String> stream2 = Stream.of("一","秒","钟");
            Stream<String> stream3 = Stream.concat(stream,stream2);
            System.out.println(stream3.count());
        }
    }
    

    运行结果如下:

    二. 方法引用

    在使用Lambda表达式的时候,我们实际上传递进去的代码就是一种解决方案:拿什么参数做什么操作。那么考虑一种情况:如果我们在Lambda中所指定的操作方案,已经有地方存在相同方案,那是否还有必要再写重复逻辑?

    1. 冗余的Lambda场景

    来看一个简单的函数式接口以应用Lambda表达式:

    @FunctionalInterface
    public interface Printable {
    	void print(String str);
    }
    

    在 Printable 接口当中唯一的抽象方法 print 接收一个字符串参数,目的就是为了打印显示它。那么通过Lambda来使用它的代码很简单:

    package Demo02;
    
    public class Demo14Simple {
        private static void printString(Printable p){
            p.print("hello,world");
        }
    
        public static void main(String[] args){
            printString(s->System.out.println(s));
        }
    }
    

    运行结果如下:

    其中 printString 方法只管调用 Printable 接口的 print 方法,而并不管 print 方法的具体实现逻辑会将字符串打印到什么地方去。而 main 方法通过Lambda表达式指定了函数式接口 Printable 的具体操作方案为:拿到String(类型可推导,所以可省略)数据后,在控制台中输出它。

    2. 问题分析

    这段代码的问题在于,对字符串进行控制台打印输出的操作方案,明明已经有了现成的实现,那就是 System.out对象中的 println(String) 方法。既然Lambda希望做的事情就是调用 println(String) 方法,那何必自己手动调用呢?

    3. 用方法引用改进代码

    能否省去Lambda的语法格式(尽管它已经相当简洁)呢?只要“引用”过去就好了:

    package Demo02;
    
    public class Demo15PrintRef {
        private static void printString(Printable p){
            p.print("hello.world");
        }
    
        public static void main(String[] args){
            printString(System.out::println);
        }
    }
    

    请注意其中的双冒号 :: 写法,这被称为“方法引用”,而双冒号是一种新的语法。

    4. 方法引用符

    双冒号 :: 为引用运算符,而它所在的表达式被称为方法引用。如果Lambda要表达的函数方案已经存在于某个方法的实现中,那么则可以通过双冒号来引用该方法作为Lambda的替代者。

    4.1 语义分析

    例如上例中, System.out 对象中有一个重载的 println(String) 方法恰好就是我们所需要的。那么对于printString 方法的函数式接口参数,对比下面两种写法,完全等效:

    1. Lambda表达式写法: s -> System.out.println(s);
    2. 方法引用写法: System.out::println

    第一种语义是指:拿到参数之后经Lambda之手,继而传递给 System.out.println 方法去处理。
    第二种等效写法的语义是指:直接让 System.out 中的 println 方法来取代Lambda。两种写法的执行效果完全一样,而第二种方法引用的写法复用了已有方案,更加简洁。

    注:Lambda 中 传递的参数 一定是方法引用中 的那个方法可以接收的类型,否则会抛出异常

    4.2 推导与省略

    如果使用Lambda,那么根据“可推导就是可省略”的原则,无需指定参数类型,也无需指定的重载形式——它们都将被自动推导。而如果使用方法引用,也是同样可以根据上下文进行推导。

    函数式接口是Lambda的基础,而方法引用是Lambda的孪生兄弟。
    下面这段代码将会调用 println 方法的不同重载形式,将函数式接口改为int类型的参数:

    package Demo02;
    
    @FunctionalInterface
    public interface PrinttableInteger {
        void print(int str);
    }
    

    由于上下文变了之后可以自动推导出唯一对应的匹配重载,所以方法引用没有任何变化:

    package Demo02;
    
    public class Demo16PrintOverload {
        private static void printString(PrintableInteger p){
            p.print(1024);
        }
    
        public static void main(String[] args){
            printString(System.out::println);
        }
    }
    

    运行结果如下:

    这次方法引用将会自动匹配到 println(int) 的重载形式。

    5. 通过对象名引用成员方法

    这是最常见的一种用法,与上例相同。如果一个类中已经存在了一个成员方法:

    package Demo02;
    
    public class MethodRefObject {
        public void printUpperCase(String str){
            System.out.println(str.toUpperCase());
        }
    }
    

    函数式接口仍然定义为:

    package Demo02;
    
    @FunctionalInterface
    public interface Printable {
        void print(String str);
    }
    

    那么当需要使用这个 printUpperCase 成员方法来替代 Printable 接口的Lambda的时候,已经具有了MethodRefObject 类的对象实例,则可以通过对象名引用成员方法,代码为:

    package Demo02;
    
    public class Demo17MethodRef {
        private static void printString(Printable p){
            p.print("hello");
        }
    
        public static void main(String[] args){
            MethodRefObject mro = new MethodRefObject();
            printString(mro::printUpperCase);
        }
    }
    

    运行结果如下:

    6. 通过类名称引用静态方法

    由于在 java.lang.Math 类中已经存在了静态方法 abs ,所以当我们需要通过Lambda来调用该方法时,有两种写法。首先是函数式接口:

    package Demo02;
    
    @FunctionalInterface
    public interface Calcable {
        int calc(int num);
    }
    

    第一种写法是使用Lambda表达式:

    package Demo02;
    
    public class Demo18Lambda {
        public static void method(int num,Calcable c){
            System.out.println(c.calc(num));
        }
    
        public static void main(String[] args){
            method(-10,num->Math.abs(num));
        }
    }
    

    运行结果如下:

    但是使用方法引用的更好写法是:

    package Demo02;
    
    public class Demo19MethodRef {
        private static void method(int num,Calcable c){
            System.out.println(c.calc(num));
        }
    
        public static void main(String[] args){
            method(-12,Math::abs);
        }
    }
    

    运行结果如下:

    在这个例子中,下面两种写法是等效的:

    1. Lambda表达式: n -> Math.abs(n)
    2. 方法引用: Math::abs

    7. 通过super引用成员方法

    如果存在继承关系,当Lambda中需要出现super调用时,也可以使用方法引用进行替代。首先是函数式接口:

    package Demo02;
    
    @FunctionalInterface
    public interface Greetable {
        void greet();
    }
    

    然后是父类 Human 的内容:

    package Demo02;
    
    public class Human {
        public void sayHello(){
            System.out.println("Hello!");
        }
    }
    

    最后是子类 Man 的内容,其中使用了Lambda的写法:

    package Demo02;
    
    public class Man extends Human{
        @Override
        public void sayHello(){
            System.out.println("大家好!我是Man!");
        }
    
        public void method(Greetable g){
            g.greet();
        }
    
        public void show(){
            //调用method方法,使用Lambda表达式
            method(()->{
            //创建Human对象,调用sayHello方法
                    new Human().sayHello();});
            //简化Lambda
            method(()->new Human().sayHello());
            //使用super关键字代替父类对象
            method(()->super.sayHello());
        }
    }
    

    但是如果使用方法引用来调用父类中的 sayHello 方法会更好,例如另一个子类 Woman :

    package Demo02;
    
    public class Man extends Human{
        @Override
        public void sayHello(){
            System.out.println("大家好!我是Man!");
        }
    
        public void method(Greetable g){
            g.greet();
        }
    
        public void show(){
           method(super::sayHello);
        }
    }
    

    在这个例子中,下面两种写法是等效的:

    1. Lambda表达式: () -> super.sayHello()
    2. 方法引用: super::sayHello

    8. 通过this引用成员方法

    this代表当前对象,如果需要引用的方法就是当前类中的成员方法,那么可以使用“this::成员方法”的格式来使用方法引用。首先是简单的函数式接口:

    package Demo02;
    
    @FunctionalInterface
    public interface Richable {
        void buy();
    }
    

    下面是一个丈夫 Husband 类:

    package Demo02;
    
    public class Husband {
        public void marry(Richable r){
            r.buy();
        }
    
        public void beHappy(){
            marry(()->System.out.println("买套房子!"));
        }
    }
    

    开心方法 beHappy 调用了结婚方法 marry ,后者的参数为函数式接口 Richable ,所以需要一个Lambda表达式。但是如果这个Lambda表达式的内容已经在本类当中存在了,则可以对 Husband 丈夫类进行修改:

    package Demo02;
    
    public class Husband {
        private void buyHouse(){
            System.out.println("买套房子!");
        }
    
        public void marry(Richable r){
            r.buy();
        }
    
        public void beHappy(){
            marry(()->this.buyHouse());
        }
    }
    

    如果希望取消掉Lambda表达式,用方法引用进行替换,则更好的写法为:

    package Demo02;
    
    public class Husband {
        private void buyHouse(){
            System.out.println("买套房子!");
        }
    
        public void marry(Richable r){
            r.buy();
        }
    
        public void beHappy(){
            marry(this::buyHouse);
        }
    }
    

    在这个例子中,下面两种写法是等效的:

    1. Lambda表达式: () -> this.buyHouse()
    2. 方法引用: this::buyHouse

    9. 类的构造器引用

    由于构造器的名称与类名完全一样,并不固定。所以构造器引用使用 类名称::new 的格式表示。首先是一个简单的 Person 类:

    package Demo02;
    
    public class Person {
        private String name;
    
        public Person(String name){
            this.name = name;
        }
    
        public String getName(){
            return name;
        }
        
        public void setName(String name){
            this.name = name;
        }
    }
    

    然后是用来创建 Person 对象的函数式接口:

    package Demo02;
    
    @FunctionalInterface
    public interface PersonBuilder {
        Person builderPerson(String name);
    }
    

    要使用这个函数式接口,可以通过Lambda表达式:

    package Demo02;
    
    public class Demo20Lambda {
        public static void printName(String name,PersonBuilder pb){
            System.out.println(pb.builderPerson(name).getName());
        }
    
        public static void main(String[] args){
            printName("私忆一秒钟",name-> new Person(name));
        }
    }
    

    运行结果如下:

    但是通过构造器引用,有更好的写法:

    package Demo02;
    
    public class Demo20Lambda {
        public static void printName(String name,PersonBuilder pb){
            System.out.println(pb.builderPerson(name).getName());
        }
    
        public static void main(String[] args){
            printName("私忆一秒钟",Person::new);
        }
    }
    

    运行结果如下:

    在这个例子中,下面两种写法是等效的:

    1. Lambda表达式: name -> new Person(name)
    2. 方法引用: Person::new

    10. 数组的构造器引用

    数组也是 Object 的子类对象,所以同样具有构造器,只是语法稍有不同。如果对应到Lambda的使用场景中时,需要一个函数式接口:

    package Demo02;
    
    @FunctionalInterface
    public interface ArrayBuilder {
        int[] builderArray(int length);
    }
    

    在应用该接口的时候,可以通过Lambda表达式:

    package Demo02;
    
    public class Demo21ArrayInitRef {
        public static int[] initArray(int length,ArrayBuilder ab){
            return ab.builderArray(length);
        }
    
        public static void main(String[] args){
            int[] arr = initArray(10,len->new int[len]);
        }
    }
    

    但是更好的写法是使用数组的构造器引用:

    package Demo02;
    
    public class Demo21ArrayInitRef {
        public static int[] initArray(int length,ArrayBuilder ab){
            return ab.builderArray(length);
        }
    
        public static void main(String[] args){
            int[] arr = initArray(10,int[] :: new );
        }
    }
    

    在这个例子中,下面两种写法是等效的:

    1. Lambda表达式: length -> new int[length]
    2. 方法引用: int[]::new
    展开全文
  • Java基础知识面试题(2020最新版)

    万次阅读 多人点赞 2020-02-19 12:11:27
    文章目录Java概述何为编程什么是Javajdk1.5之后的三大版本JVM、JRE和JDK的关系什么是跨平台性?原理是什么Java语言有哪些特点什么是字节码?采用字节码的最大好处是什么什么是Java程序的主类?应用程序和小程序的...
  • 判断对象是否存活的算法:1、引用计数算法 给对象添加一个引用计数器,当有一个地方引用它时,计数器值就加1;当引用失效时,计数器就减1;任何时候计数器都为0的对象就是不可能再使用的。引用计数器算法...
  • 基于Java8详细介绍了Lambda表达式的语法与使用,以及方法引用、函数式接口、Lambda复合等Java8的新特性!
  • java中什么方法以及方法怎么定义

    千次阅读 多人点赞 2018-05-06 16:21:31
    我们先来理解一下什么叫做方法,通俗一点来说:我们的日常生活方法可以理解为要做某件事情,而采取的解决办法。如:小明同学路边准备坐车来学校学习。这就面临着一件事情(坐车到学校这件事情)需要解决,...
  • Java面试题大全(2020版)

    万次阅读 多人点赞 2019-11-26 11:59:06
    发现网上很多Java面试题都没有答案,所以花了很长时间搜集...1. JDK 和 JRE 有什么区别? JDK:Java Development Kit 的简称,java 开发工具包,提供了 java 的开发环境和运行环境。 JRE:Java Runtime Environ...
  • (1)用static修饰的方法称为静态方法,修饰变量则为静态变量,又分别叫做类方法或者类变量。这两个从属于类,没有创建实例也可以使用。 &amp;amp;nbsp; &amp;amp;nbsp; &amp;amp;nbsp; &amp;amp;...
  • lambda方法引用总结——烧脑吃透

    千次阅读 2017-06-08 15:49:37
    需要函数参数的方法中,我们可以把另一个同类型的方法直接传入,这称为方法引用的绑定。类似于C语言的函数指针。lambda表达式可以替代方法引用;或者说方法引用是lambda的一种特例,方法引用不可以控制传递参数...
  • 前面两篇文章了解到Java对象实例是如何...下面详细了解Java堆的Java对象是如何访问定位的:先来了解reference类型数据是什么,再来了解两种访问方式:使用句柄或者使用直接指针(HotSpot虚拟机使用直接指针)。
  • 浅谈一下JAVA对象,对象引用以及对象赋值

    万次阅读 多人点赞 2013-09-19 00:50:29
    今天有班级同学问起JAVA对象的引用什么。正好趁着这次机会,自己总结一下JAVA对象,对象引用以及对象赋值。自己总结了所看到的网上相关方面的不少帖子,整理汇总形成下面的文章。   Java对象及其引用  初学Java...
  • 我的上一篇博客 深入理解Java什么内部类可以访问外部类的成员 , 通过使用javap工具反编译内部类的字节码, 我们知道了为什么内部类可以访问外部类的成员, 其实是编译器编译内部类的class文件时,偷偷...
  • C语言引用,以及传递引用,数组传递

    万次阅读 多人点赞 2019-04-08 20:04:48
    详解c++指针的指针和指针的引用 ...展示一下使用指针的指针和指针的引用修改传递给方法的指针,以便更好的使用它。(这里说的指针的指针不是一个二维数组) 为什么需要使用它们 ...如果我们在方法内部修改...
  • 本文通过对象的创建步骤的检查加载->分配内存->内存空间初始化->设置->对象初始化,对象的内存布局,什么是垃圾的两种算法以及四种引用,讲述JVM对象及引用
  • 方法引用由来 从JDK8新特性一文 『Lambda表达式』模块,当我们学习了 1.Lambda表达式 2.Lambda表达式原理分析 3.Lambda表达式----常用的内置函数式接口,学习了Lambda 表达式以及它的简写形式,并使用 Lambda ...
  • Java引用对象

    千次阅读 2018-12-11 10:09:03
    借助指针切换(pointer handoffs)等编码实践或者Purify等工具,我认为自己对C风格的内存管理已经得心应手了,甚至已经不记得上次发生内存泄露是什么时候了。所以起初我接触到Java的自动内存管理时有些不屑,但很快就...
  • 当一个对象当作参数传递到一个方法后,此方法可改变这个对象的属性,并可返回变化后的结果,那么这里到底是值传递还是引用传递?
  • js面试题

    千次阅读 多人点赞 2019-04-09 19:42:32
    JavaScript 的组成 JavaScript 由以下三部分...BOM(浏览器对象模型):提供了浏览器窗口之间进行交互的对象和方法 JS 的基本数据类型和引用数据类型 基本数据类型:undefined、null、boolean、number、string、s...
  • PDF文件交叉引用流对象(cross-reference stream)的解析方法   1 介绍 PDF-1.5版本[1]之前,对象的交叉引用信息是存储交叉引用表(cross-reference table)的。PDF-1.5版本之后,引进了交叉引用流(cross-...
  • Java方法和类详解

    万次阅读 2019-10-18 18:31:50
    方法和类详解控制类成员函数的访问向方法传递对象返回对象方法重载重载构造函数递归static关键字static代码块嵌套类和内部类varargs(可变长度实参) 控制类成员函数的访问 Java的访问控制符有三个: public:...
  • 下面先来了解Java虚拟机垃圾回收的基础内容:如何判断对象是... 介绍相关的垃圾回收基础算法:引用计数算法、可达性分析算法,以及说明finalize()方法作用,最后再来说说HotSpot虚拟机实现对象可达性分析的一些问题。
  • JNI,Java Native Interface,是 native code 的编程接口。JNI 使 Java 代码程序可以与 native code 交互——...JNI 编程软件开发运用广泛,其优势可以归结为以下几点:利用 native code 的平台相关性,平台相...
  • 什么是值传递和引用传递?

    千次阅读 2018-08-14 08:10:16
    值类型默认存放,但当值类型是在引用类型声明的时候,则存放其所在的引用类型的堆。 &lt;2&gt;引用类型存放,其的内存地址放在栈。   2.参数传递方式: &lt;1&gt;值...
  • java的经典问题:传值与传引用

    万次阅读 多人点赞 2014-04-04 16:30:53
    记得刚开始学编程那会儿,老师教导,所谓参数,有形式参数和实际参数之分,参数列表写的那些东西都叫形式参数,实际调用的时候,它们会实际参数所替代。  编译程序不可能知道每次调用的实际参数都是什么,...
  • java方法区究竟存储了什么

    万次阅读 多人点赞 2016-05-14 13:40:46
    首先要说明的是,此文章转载自 ...谢谢作者。另外,这里ps一下,Class对象是存放在堆区的,不是方法...Class对象是加载的最终产品,类的方法代码,变量名,方法名,访问权限,返回值等等都是在方法区的)才是存在方法区的
  • 如果声明方法时包含来了形参声明,则调用方法时必须给这些形参指定...指向同一个对象,对象的内容可以在被调用的方法中改变,但对象的引用(不是引用的副本)是永远不会改变的。  Java参数,不管是原始类型还是引用类型
  • 首先谈引用Labview中长称为引用句柄,Windows编程引用句柄指的是指向指针的指针,换句话说,引用句柄保存的是其他一些数据类型的地址,例如窗口句柄。 Labview,**控件的引用句柄指的也是指向特定...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 308,394
精华内容 123,357
关键字:

引用在说明方法中被称为什么