-
2019-03-05 10:37:51
1.什么叫引用?
答:1. 首先,你要明白什么是变量。变量的实质是一小块内存单元。这一小块内存里存储着变量的值
比如int a = 1;
a就是变量的命名,1就是变量的值。
而当变量指向一个对象时,这个变量就被称为引用变量
比如A a =new A();
a就是引用变量,它指向了一个A对象,也可以说它引用了一个A对象。我们通过操纵这个a来操作A对象。 此时,变量a的值为它所引用对象的地址。其中:
(1)引用数据类型包括:类、接口类型、数组类型、枚举类型、注解类型,字符串型;
(2)引用数据型在被创建时,首先要在栈上给其引用(句柄)分配一块内存,而对象的具体信息都存储在堆内存上,然后由栈上面的引用指向堆中对象的地址。
(3)引用数据类型为java两大数据类型之一。
参考:https://blog.csdn.net/qq_14989227/article/details/79589342- 在java实现某个功能的时候,会将类实例化成对象,然后jvm再对实例化后的对象进行操作。
实例化后的对象可以赋值,也可以引用。赋值时,java会开辟一个新的地址用于存放变量,而引用则不开辟地址。
String a = “a”;
String b = “a”;
将String类实例化成对象a与b,并赋值
String c = new String(“a”);
将c指向new出来的String型对象"a"
System.out.println(a == b);
System.out.println(a == c);
由于a与b都为对象赋值,打印出来的结果应该是true
而c是引用对象“a”,所以打印出来的结果应该是false
对象:一个类的具体实例化;引用:对象的别名,使用该别名可以存放该对象,编译器不会为引用分配空间,新对象与源对象共用一个存储地址空间;引用的生命周期是它所引用的对象的生命周期,函数里返回一个局部对象的引用是很危险的
参考:https://zhidao.baidu.com/question/412004880.html
2. 引用的描述
答:
(1)引用不是指针
(2)引用本身也是一种数据类型
(3)引用不是对象本身
(4)一个对象可以被多个引用指引,例如:String对象,String a1=“abc”;String b1=“ab”更多相关内容 - 在java实现某个功能的时候,会将类实例化成对象,然后jvm再对实例化后的对象进行操作。
-
十种说明方法造句
2020-12-21 14:01:16怎么用十种说明方法造句,在线等,急举例子: 他是个热心肠的人,有一次下雨天他把伞借给一个老奶奶,自己却淋着雨。列数字: 赵州桥非常雄伟,全长50。82米,两端宽9。6米,中部略窄,宽9米。打比方: 弯弯的月亮像...怎么用十种说明方法造句,在线等,急
举例子: 他是个热心肠的人,有一次下雨天他把伞借给一个老奶奶,自己却淋着雨。
列数字: 赵州桥非常雄伟,全长50。
82米,两端宽9。
6米,中部略窄,宽9米。
打比方: 弯弯的月亮像小船。
作比较: 新建的桥比老桥坚固得多。
下定义: 记叙文是以人物经历和事物发展变化为主要内容的一种文体形式。
分类别: 作文体裁丰富多样,分为记叙文、说明文、议论文、散文等。
作诠释: 这个物品的表面十分光滑,就是为了减少摩擦力。
作假设: 我们只有一个地球,如果它被破坏了,我们将别无去处。
作引用: 唐朝的张嘉贞曾这样评价赵州桥——“制造奇特,人不知其所以为”
摹状貌: 他十分帅气,有高高的鼻梁,浓浓的眉毛,还有一双炯炯有神的大眼睛。
举例子怎么造句??至少3句!
用举例子造句
1、对于这种比较专业的命令,我想还是多举举例子比较好让新手理解。
2、举例子:具体真切地说明了事物的**特点。
3、在这里,我就不想举例子了。
4、我们可以举例子说明。
所举例子属实吗?
5、举例子是为了支撑观点,使之更具说服力。
6、您在演讲中,经常打比喻、举例子,请问有哪些好处?
7、我每天讲课举例子已经很烦了,大家可以自己思考一下这句话。
8、他运用了打比方、举例子的说明方法。
如何用五种说明方法造句?
列数字:
这座塔占地300平方米,高100米。
作比较:
春天的雨细腻柔媚,夏天的雨粗犷热烈。
举例子:
晕能预示天气。
比如,在新疆地区,出现晕就代表将要下雨。
(根据大的范围列举相应的例子)
下定义:
大气层中的对流层就是指温度上低下高的紧贴地面因而能形成对流的一层。
(定义要求完整,即定义的对象与所下定义的外延要相等,并且要从一个方面完整地揭示概念的全部内涵)
分类别:
通常情况下,我们将云分为预示晴朗的云和预示阴雨的云。
(把一个大的范围分为一个个小的范围)
十种说明方法的区分
说明方法及作用 1、举例子:举有代表性的例子更具体地说明事物某方面的特征。
作用:使说明对象更具体、形象、确切,增强说明力,便于读者理解。
例:犹太人的智商很高,其中有许多杰出人氏。
比例,爱因斯坦,弗洛伊德,马。
分析怎样造句??????
??句子是语言运用的基本单位,它由词或词组构成,能表达一个完整的意思,如告诉别人一件事,提出一个问题,表示要求或者制止,表示某种感慨。
它的句尾应该用上句号、问号或感叹号。
造句的方法一般有以下几种。
??1。
在理解词义的基础上加以说明。
如用“瞻仰”造句,可以这样造:“我站在广场上瞻仰革命烈士纪念碑。
”因为“瞻仰”是怀着敬意抬头向上看。
??2。
用形容词造句,可以对人物的动作、神态或事物的形状进行具体的描写。
如用“鸦雀无声”造句:“教室里鸦雀无声,再也没有人说笑嬉闹,再也没有人随意走动,甚至连大气都不敢出了。
”这就把“鸦雀无声”写具体了。
??3。
有的形容词造句可以用一对反义词或用褒义词贬义词的组合来进行,强烈的对比能起到较好的表达作用。
如用“光荣”造句:“讲卫生是光荣的,不讲卫生是可耻的。
”用“光荣”与“可耻”作对比,强调了讲卫生是一种美德。
??4。
用比拟词造句,可以借助联想、想象使句子生动。
如用“仿佛”造句:“今天冷极了,风刮在脸上仿佛刀割一样。
”
??5。
用关联词造句,必须注意词语的合理搭配。
这就需要在平时学习中,把关联词的几种类型分清并记住。
??6。
先把要造句的词扩展成词组,然后再把句子补充完整。
如用“增添”造句,可以先把“增添”组成“增添设备”、“增添信心”或“增添力量”,然后再造句就方便多了。
??最后造句要自己脑筋,不要抄书上现成的句子。
用打比方的说明方法造句
我们的居民楼都盖成火柴盒的样子,没有任何新意。
怎么用“作比较”和“打比方”的说明方法来造句?
1。
我们从地球上看太阳,觉得它就像一个盘子那么大,实际上一个太阳相当于30万个地球啊!
2。
菊花和牡丹在温馨的花园里开出炫烂的花朵,这固然是美的,但在没也比不过挺立在寒风傲雪中灿烂盛开的红梅!
3。
我国发现过一头近四万公斤重的鲸,约十七米长,一条舌头就有十几头大肥猪那么重。
4。
我们学校的教学楼非常地壮观,整体看来像一个举人一样矗立在那里。
5。
同茫茫宇宙相比,地球是渺小的。
6。
塞特凯达斯瀑布在群山之间无奈的垂下了头,想成命垂危的老人,形容枯槁,奄奄一息。
7。
岸边的华灯倒映在湖中,宛如无数的银蛇在游动。
8。
马路上一串串明亮的车灯,如同闪光的长河奔流不息。
9。
春天的雨细腻柔媚,夏天的雨粗犷热烈。
10。
牡丹高贵典雅,但与菊相比,就少了一份淡然的美。
说明方法有哪些并造句
说明方法有下定义,分类别、举例子、列数字、作比较和打比方。
一目十行造句
初读课文时一目十行,怎么能把字音读准确呢?“一目十行”是一种粗略的读书方法,它能帮我们在短时间内了解文章的大意。
小明的理解能力很强,看书一般是一目十行的看。
-
Java8—Lambda表达式、方法引用、默认方法的详细介绍【一万字】
2020-08-27 09:29:34基于Java8详细介绍了Lambda表达式的语法与使用,以及方法引用、函数式接口、Lambda复合等Java8的新特性!基于Java8详细介绍了lambda表达式的语法与使用,以及方法引用、函数式接口、lambda复合等Java8的新特性!
文章目录
1 Lambda的概述
面向对象的语言强调“必须通过对象的形式来做事情”,做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情。
无论什么情况,当我们在一个方法中需要调用另一个方法的时候,传递的参数必须是一个含有该方法的对象,对象作为一等公民!对于某些可以独立的单个方法(行为),比如比较的方法,在面向对象的程序设计中,同样必须使用一个对象来进行封装,比如Comparable、Comparator,虽然Java中已经使用接口这种更加抽象的类型来封装“比较”这种方法(行为),但是在一个方法中调用比较的方法的时候,我们仍然需要传递一个接口的实现类的对象,然后再方法中调用这个对象的方法,就会很麻烦!因为实际上我们只需要进行比较的这个方法(行为),它却必须要传递一个对象进来!
/** * 比较对象的方法 * * @param comparator 比较器 * @param i i * @param j j */ public static int cmp(Comparator comparator, int i, int j) { return comparator.compare(i, j); } @Test public void testJava() { //传递一个对象 int cmp = cmp(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1 - o2; } }, 1, 2); System.out.println(cmp); }
如上图,我们有一个比较对象的方法cmp,在使用传统Java代码编程时,即使最简单的方式,也需要传递一个匿名内部类对象进去,但是我们实际上需要的只是比较的行为而已,并不需要对象!
面向对象的编程思想自有它的好处,比如封装性,可重用性,多态性。但是程序设计的世界里,想要依靠一种方法打遍天下并且还是最优的解,那几乎是不可能的!Java 8开始,支持lambda表达式,就是为了解决面向对象编程思想在某些时候(比如单个方法的调用)显得很笨重而又啰嗦!
lambda表达式是一种函数式的编程思想,尽量忽略面向对象的复杂语法。函数式的编程思想中,函数作为一等公民,这里的函数可以类比Java中的方法,描述的是一种行为!当一个方法(函数)中调用另一个方法时,直接将该方法(行为)作为参数传递即可。这样相比于面向对象的编程思想来说,可以让编程更加简单!
对于上面的代码,我们使用lambda表达式改造之后,如下所示:@Test public void testLambda() { int cmp = cmp((Comparator<Integer>) (o1, o2) -> o1 - o2, 1, 2); System.out.println(cmp); }
可以看到,lambda表达式的应用让代码编程非常简单明了,我们直接将比较的行为作为参数传递给了cmp方法,连匿名对象都没有了!
2 函数式接口
简单的说,函数式接口(Functional Interface)就是只定义一个抽象方法的接口,并且只有在函数式接口中才能使用lambda表达式。为此,Java 8的时候新增加了一个@FunctionalInterface注解,用来表明某个接口是函数式接口。注意一个函数式接口可以选择加上该注解也可以不加上该注解,这个注解简单的说可以作为一种检验!
lambda表达式实际上就是对函数式接口的唯一抽象方法起作用的,即相当于可以把抽象方法的实现作为函数式接口的具体实现的实例来当作参数传递!这类似于匿名内部类!
Java8之前就有许多函数式接口,比如Runnable、Callable、Comparator、Comparable……,在Java8的时候,为了更好的支持lambda,新增了一个java.util.function包,这个包下面的有很多的接口,这些接口全部都是函数式接口,它们都用于描述某个行为,方便lambda的使用!
下面我们将介绍常用的四种接口:Consumer消费型接口、Supplier供给型接口、Function函数型接口、Predicate断言型接口,最后会附上大部分函数式接口的不同行为和功能!可能某些案例的lambda表达式看不太懂,不过没关系,下一节将会讲解lambda的语法!
2.1 Consumer消费型接口
@FunctionalInterface public interface Consumer<T> { /** * @param t 输入参数 */ void accept(T t); //…… }
Consumer接口中有一个accept抽象方法,它用于接收一个泛型参数T,然而并没有返回值,顾名思义,就是对传递的参数进行“消费”,没有输出,就像消费者一样!
我们可以将其应用在对某些输入数据的处理但是不需要输出的情况中!下面的案例中,我们需要对集合中的所有int元素进行+1然后输出的操作:
/** * @author lx */ public class ConsumerTest { public static void main(String[] args) { //一个初始化集合 List<Integer> objects = new ArrayList<>(); objects.add(1); objects.add(2); objects.add(3); //对集合数据进行 加1然后输出的操作 consume(objects, i -> System.out.println(i + 1)); } /** * 使用Consumer对集合元素进行操作的方法 * * @param list 需要操作的集合 * @param consumer 对元素的具体的操作,在调用的时候传递某个动作就行了 */ private static <T> void consume(List<T> list, Consumer<T> consumer) { for (T t : list) { consumer.accept(t); } } }
2.2 Supplier供给型接口
@FunctionalInterface public interface Supplier<T> { /** * @return 获取一个返回结果 */ T get(); }
Supplier接口中有一个get抽象方法,它不接收任何参数,但是返回一个T类型的结果,顾名思义,就是没有输入,只有输出,就像生产者一样!
我们可以将其应用在创建某些对象、获取数据数据的情况中。下面的案例中,我们需要用集合收集10个随机数:
/** * @author lx */ public class SupplierTest { public static void main(String[] args) { //一个初始化集合 List<Integer> objects = new ArrayList<>(); //我们需要填充10个随机数,Supplier是一个获取随机数的动作 Random random = new Random(); supplier(objects, 10, () -> random.nextInt(10)); //输出集合数据 System.out.println(objects); } /** * 填充集合数据的方法 * * @param list 需要填充的集合 * @param count 需要填充的数量 * @param supplier 获取数据的的操作,在调用的时候传递某个动作就行了 */ private static <T> void supplier(List<T> list, int count, Supplier<T> supplier) { for (int i = 0; i < count; i++) { list.add(supplier.get()); } } }
2.3 Function< T, R >函数型接口
@FunctionalInterface public interface Function<T, R> { /** * 将指定函数应用于给定的参数。 * * @param t 函数参数 * @return 函数结果 */ R apply(T t); }
Function接口中有一个apply抽象方法,它接收T类型的参数,返回一个R类型的结果,顾名思义,就是一个参数T到R的映射操作,就像一个函数一样!
我们可以将其应用在对某个输入对象进行变换、操作然后输出另一个对象(也可以是自己)的情况中。下面的案例中,我们需要对集合中的所有int元素进行自增1的操作:
/** * @author lx */ public class FunctionTest { public static void main(String[] args) { //一个初始化集合 List<Integer> objects = new ArrayList<>(); objects.add(1); objects.add(2); objects.add(3); //对集合中的数据进行 自增1的操作 function(objects, i -> ++i); //输出集合数据 System.out.println(objects); } /** * 使用Function对集合元素进行操作的方法 * * @param list 需要操作的集合 * @param function 对元素的具体的函数操作,在调用的时候传递某个动作就行了 */ private static <T> void function(List<T> list, Function<T, T> function) { for (int i = 0; i < list.size(); i++) { //将通过传入的函数操作获取的结果替换原来的集合对应的数据 list.set(i, function.apply(list.get(i))); } } }
2.4 Predicate断言型接口
@FunctionalInterface public interface Predicate<T> { /** * 对给定的参数进行断言的方法 * * @param t 输入参数 * @return 如果参数符合规则,那么返回true,否则返回false */ boolean test(T t); }
2.5 其他接口以及功能
java.util.function包中的大多数其他函数式接口都是一个特性化的接口,即它们的功能和上面的四大接口都差不多,区别可能是参数数量和类型以及返回值类型!
函数描述符:用来描述函数的参数以及返回值的类型,()表示无参,void表示无返回值,中间使用->连接。
函数式接口 函数描述符 特性化接口 Predicate< T > T->boolean IntPredicate,LongPredicate, DoublePredicate Consumer< T > T->void IntConsumer,LongConsumer, DoubleConsumer Function< T,R > T->R IntFunction< R >,IntToDoubleFunction,IntToLongFunction,LongFunction< R >,LongToDoubleFunction,LongToIntFunction,DoubleFunction< R >,ToIntFunction< T >,ToDoubleFunction< T >,ToLongFunction< T > Supplier< T > ()->T BooleanSupplier,IntSupplier, LongSupplier UnaryOperator< T > T->T IntUnaryOperator,LongUnaryOperator,DoubleUnaryOperator BinaryOperator< T > (T,T)->T IntBinaryOperator,LongBinaryOperator,DoubleBinaryOperator BiPredicate< L,R > (L,R)->boolean BiConsumer< T,U > (T,U)->void ObjIntConsumer< T >,ObjLongConsumer< T >,ObjDoubleConsumer< T > BiFunction< T,U,R > (T,U)->R ToIntBiFunction< T,U >,ToLongBiFunction< T,U >,ToDoubleBiFunction< T,U > 前四个都介绍了,后面的其实都差不多,只是参数数量和类型以及返回值类型有差异:
- UnaryOperator:一元操作器,一个参数一个返回值,类似于Function,不过参数和返回值类型一致。
- BinaryOperator:二元操作器,两个参数一个返回值,类型一致。
- BiPredicate:二元断言,传递两个可以不同类型的参数,返回一个boolean类型。
- BiConsumer:二元消费,传递两个可以不同类型的参数,无返回值。
- BiFunction:二元函数,传递两个可以不同类型的参数,一个返回值可以是不同类型。
下面我们正式学习lambda的语法!
3 Lambda的语法
3.1 具体格式
lambda表达式的标准格式为:
(参数类型 参数名称, 参数类型 参数名称) ‐> { 代码语句 }
小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。-> 是新引入的语法格式,代表指向动作。大括号内的语法与传统方法体要求基本一致。
比如对Comparator接口使用匿名内部类对象和lambda表达式:
//使用传统匿名内部类 Comparator<Integer> comparable1 = new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1 - o2; } }; //使用lambda表达式标准语法 Comparator<Integer> comparable2 = (Integer o1, Integer o2) -> { return o1 - o2; };
当然,如果使用idea,那么可能会提示你这个lambda表达式还有更精简的写法。
//使用lambda表达式优化语法 Comparator<Integer> comparable3 = (o1, o2) -> o1 - o2;
可以看到,此时我们的lambda表达式更加精简了,同时也更加通俗易懂,那就是通过比较两个数的差值来比较大小!
在lambda标准格式的基础上,使用省略写法的规则为:
- 小括号内参数的类型可以省略;
- 如果小括号内有且仅有一个参,则小括号可以省略;
- 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号和return关键字及语句的分号,即无论有没有返回值,都可以省略return。
由此,我们可以知道,同一个lambda表达式可以对应不同的实际目标类型,比如下面的例子,同样的lambda表达式,却可以赋值给不同的类型!
//对应Comparator目标类型 Comparator<Integer> comparable3 = (o1, o2) -> o1 - o2; //对应BinaryOperator目标类型 BinaryOperator<Integer> binaryOperator = (o1, o2) -> o1 - o2;
实际上,lambda表达式也有自己的类型,但是它的类型是通过上下文(参数类型、返回值类型、包括泛型类型)推断得来的。在上面的Comparator的精简写法中,参数类型被省略了,因为可以通过返回值的泛型类型Integer推断出来,参数的类型也一定是Integer类型,另外通过返回的类型可以推断出这个lambda一定是Comparator类型。
在下面的lambda表达式中,参数类型同样被省略了,因为可以通过cmp方法的第一个参数可以推断出,参数类型一定是Byte类型,并且根据第一个参数的目标类型可以推断出这个lambda表达式一定是Comparator类型!
@Test public void test1() { cmp((o1, o2) -> (o1 - o2), (byte) 1, (byte) 1); } public int cmp(Comparator<Byte> comparator, byte l1, byte l2) { return comparator.compare(l1, l2); }
这里的上下文推断就类似于JDK1.7出现的针对集合的类型推断<>符号:
//JDK1.7开始,右侧构造器可以使用<>当作泛型推断 List<String> strings = new ArrayList<>();
只不过Java8的时候对类型推断做了进一步增强,使用上下文推断可以在使用Lambda表达式时用来推断合法的Lambda表达式的类型的上下文,而不必在代码中强制转型或者注明类型!可推导即可省略!
3.2 使用要求
Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:
- 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
- 使用Lambda必须具有上下文推断。也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
- 在 Lambda 表达式中不允许声明一个与局部变量同名的参数或者局部变量。
- 在 Lambda 表达式中,允许引用最终变量、静态变量、局部变量,但是只允许修改静态变量,以及对象类型的局部变量的属性(这要求后面的代码不会修改这个局部变量的引用指向),对于局部变量本身的引用指向以及基本类型的变量则不允许修改
- 对应第四条的另一种解释,lambda表达式的局部变量可以不用声明为final,但是实际上是具有隐式的final的的语义,即必须不可被后面的代码修改,否则会编译错误。
为什么会对局部变量有这些限制呢?主要是因为对象类型局部变量的引用以及基本类型的局部变量都保存栈上,存在某一个线程之中,如果Lambda可以直接访问并修改栈上的变量,并且Lambda是在另一个线程中使用的,那么使用Lambda的线程可能会在分配该变量的线程将这个变量收回之后,继续去访问该变量。因此,Java在访问栈上的局部变量时,实际上是在访问它的副本,而不是访问原始变量,从而造成线程不安全的可能,特别是并行运算的时候。但是如果局部变量仅仅被最开始赋值一次,以后不会再次变动,那就没有这种隐患了——因此就有了这个限制,即局部变量除了最开始的赋值之后都是读操作,而没有写操作,那么可以读取这个局部变量,相当于final的语义了。
由于对局部变量的限制,Lambda表达式在 Java 中又称为闭包或匿名函数。它们可以作为参数传递给方法,并且可以访问其作用域之外的变量。但是它们不能修改定义Lambda的方法的局部变量的内容,这些变量必须是隐式最终的。因此可以认为Lambda是对值封闭,而不是对变量封闭,因为可以访问局部变量,但不可修改值。为什么对象类型的额局部变量的属性可以修改呢?因为它们保存在堆中,而堆是在线程之间共享的!因此我们如果需要在lambda中修改某个基本变量,那么可以使用该变量的包装类。然后再修改属性值即可。
关于变量的测试案例如下:
//静态全局变量 static int k = 1; static AtomicInteger stinteger = new AtomicInteger(1); @Test public void test3() { int i = 1; Object o = new Object(); AtomicInteger integer = new AtomicInteger(1); //使用lambda表达式标准语法 Comparator<Integer> comparable = (Integer o1, Integer o2) -> { //在后面的语句中不会修改这个局部变量的值时,可以在lambda中访问基本局部变量,但是不可操作值 int b = i; System.out.println(i); //在后面的语句中不会修改这个对象局部变量的引用指向时,可以操作或者访问这个对象的属性 integer.addAndGet(1); integer.get(); integer.set(10); //静态变量的引用指向可以修改 stinteger = new AtomicInteger(2); //静态变量的值可以修改 k = 2; return o1 - o2; }; //在后面的语句中改变基本局部变量的值之后,lambda中对该变量的任何访问操作都将编译不通过 // i = 2; //在后面的语句中改变对象局部变量的引用指向之后,lambda中对该变量的任何访问操作都将编译不通过 //integer= new AtomicInteger(1); //在后面的语句中可以操作或者访问这个对象的属性 integer.set(15); //静态变量的引用指向可以修改 stinteger = new AtomicInteger(3); //静态变量的值可以修改 k = 3; } static class Run implements Runnable { @Override public void run() { } }
4 方法引用
到此之前,我们已经会使用Lambda表达式创建匿名方法,自己实现方法体,但是有时候,我们的Lambda表达式可能仅仅调用一个已存在的方法,而不做任何其它事,对于这种情况,通过一个方法名字来引用这个已存在的方法会更加清晰,Java 8的方法引用允许我们这样做。方法引用简单的格式通过引用一个已经存在的方法,同时实现了代码的复用,进一步简化了lambda的复杂度。
方法引用的目标很明显,因为方法可以看作一个已经存在的定义好的函数,当我们要传递的函数已经被某个方法实现了的时候,那么则可以通过双冒号"::"操作符来引用该方法作为 Lambda 的替代者。
/** 1. @author lx */ public class User { private int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public static void main(String[] args) { //获取User的age //lambda标准格式的优化写法,内部只是引用了一个方法 Function<User, Integer> userIntegerFunction1 = o -> o.getAge(); //获取User的age //使用方法引用之后的写法,更加精简 Function<User, Integer> userIntegerFunction2 = User::getAge; } }
方法引用同样可以使用传递的参数类型和参数个数进行推导。比如上面的lambda表达式可知道参数o的类型为User,同时它调用了getAge方法,返回一个Inteer。因此可以直接使用方法引用User::getAge,简化了方法的调用与参数的传递。这些都是可以推倒的,lambda遵循” 可推导即可省略”原则,或者说方法引用可以看作针对lambda的语法糖!
怎么才能将lambda表达式转换为方法引用呢?或者说什么情况才能使用方法引用呢?
- 要求Lambda 表达式的方法体中只有一句话,并且这句话就是调用另一个方法,此时就可能使用方法引用代替手动调用该方法。
- 特殊情况下,如果抽象方法的第一个参数就是内部调用该方法的实例,那么被调用的方法与函数式接口中的抽象方法的参数个数可以不相同,但是要求后面的参数和方法参数的顺序一致,类型相同(或者兼容)。如果不是这种特殊情况,那么还要求被调用的方法与函数式接口中的抽象方法的参数个数和顺序一致,类型相同(或者兼容)。
- 被调用的方法与函数式接口中的抽象方法返回值类型相同(或者兼容),与方法名无关。
方法引用有很多种,它们的语法如下,都需要遵循上面的原则:
- 类型上的静态方法引用:ClassName::methodName
- 实例上的实例方法引用:instanceReference::methodName
- 类型上的实例方法引用:ClassName::methodName
- 超类实例上的实例方法引用:super::methodName
- 构造方法引用:ClassName::new
- 数组构造方法引用:TypeClassName [ ]::new
方法引用案例如下:
/** * @author lx */ public class Person { private int age; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public static void print() { System.out.println("静态方法"); } public static Person instance() { return new Person(); } public Integer gett(Object str, Integer integer) { setAge(integer); return getAge(); } public Integer gett(int integer, Object str) { setAge(integer); return getAge(); } @Test public void test() { //一个user实例 Person user = new Person(); //类型上的实例方法引用:ClassName::methodName Function<Person, Integer> userIntegerFunction2 = Person::getAge; //类型上的实例方法引用:ClassName::methodName Consumer<Person> userConsumer2 = System.out::println; //实例上的实例方法引用:instanceReference::methodName Supplier<Integer> supplier = user::getAge; //类型上的静态方法引用:ClassName::methodName Supplier<Person> userSupplier = Person::instance; //类型上的静态方法引用:ClassName::methodName Print print = Person::print; //超类实例上的实例方法引用:super::methodName Supplier<Class> SupSupplier = super::getClass; //构造方法引用:ClassName::new Supplier<Object> newSupplier = Person::new; //数组构造方法引用:TypeClassName[]::new Function<Integer, Person[]> arrayFunction = Person[]::new; //如果内部调用gett(String str, Object integer)方法 ThFunction<Person, Integer, Integer, String> thFunction1 = new ThFunction<Person, Integer, Integer, String>() { @Override public Integer apply(Person o, Integer o2, String o3) { return o.gett(o3, o2); } }; //那么不能使用方法引用 //因为虽然抽象方法的第一个参数就是内部调用该方法的实例,后面的参数和方法参数的顺序不一致 //参数顺序不一致,即 o2 o3 -> o3 o2 ThFunction<Person, Integer, Integer, String> thFunction2 = (o, o2, o3) -> o.gett(o3, o2); //如果内部调用gett(Integer integer, Object str)方法 ThFunction<Person, Integer, Integer, String> thFunction3 = new ThFunction<Person, Integer, Integer, String>() { @Override public Integer apply(Person o, Integer o2, String o3) { return o.gett(o2, o3); } }; //那么能使用方法引用 //因为抽象方法的第一个参数就是内部调用该方法的实例,后面的参数和方法参数的顺序一致,类型兼容 //参数顺序一致,即 o2 o3 -> o2 o3 ThFunction<Person, Integer, Integer, String> thFunction4 = Person::gett; } /** * 自定义函数式接口,无参数无返回值 */ @FunctionalInterface public interface Print { /** * 输出 */ void print(); } @FunctionalInterface public interface ThFunction<T, U, R, K> { R apply(T t, U u, K k); } }
5 默认方法和静态方法
5.1 概述
此前,Java中的接口不能有非抽象方法,并且实现接口的类必须为接口中定义的每个方法提供一个实现,一旦类库的设计者需要更新接口,向其中加入新的方法,这种方式就会出现问题,这会导致所有的实现类必须实现新的方法,虽然我们可以提供一个骨干实现的抽象类,但是仍然不能根本的解决问题,比如其他Guava和Apache Commons提供的集合框架,会同时修改大量代码!
Java8开始,接口中新增的方法可以不需要实现类必须实现,因为接口支持两种新类型的方法及其实现,一种是静态方法,通过static关键字标识,表示这个方法可以通过接口直接调用,这个方法时是属于该接口的;另一个就是非常重要的默认方法,通过default关键字标识,并且接口提供了方法的默认实现,实现接口的类如果不显式地提供该方法的具体实现,就会自动继承默认的实现。这就类似于继承了一个普通方法,这样每次接口新增的方法可以设置为默认方法,它的实现类也不再需要改动代码,保证新方法和源代码的兼容!
实际上静态方法和默认方法被大量的用来支持lambda表达式的复杂写法与复合逻辑,后面我们会介绍到!
5.2 问题及解决
在Java8之前,一个类可以实现多个接口,即使有同名方法也没关系,因此抽象方法没有具体的行为,子类必须有针对抽象方法自己的实现。Java8之后,由于接口拥有了默认方法,也就是说接口提供了方法的默认行为,子类可以不选择实现而直接使用接口提供的实现。
实际上Java8接口允许了默认方法之后,Java已经在某种程度上实现了多继承,所以不光带来了多重继承的好处,还带来了多重继承的问题。如果一个类实现的多个接口中都具有相同方法签名的默认方法,那么这个实现类将无法通过方法签名选择具体调用哪一个接口的默认实现,此时就可能会出现问题!
如果一个类使用相同的函数签名从多个地方(比如另一个类或接口)继承了非抽象方法,通过下面规则尝试判断具体调用哪一个方法:
- 本类重写的方法优先级最高。
- 否则,一个类同时实现了类或者接口,并且类和接口具有相同的签名的方法,那么父类中声明的方法的优先级高于任何接口中声明的默认方法的优先级。
- 否则,那么子接口的优先级更高:函数签名相同时,优先选择拥有最具体实现的默认方法的接口,即如果B接口继承了A接口,那么B接口就比A更加具体。
- 否则,继承了多个接口的类必须显式指定的调用某个父接口的默认方法实现。
其他注意:
- 如果一个类实现了抽象类和接口,并且接口中具有和抽象类中的抽象方法同样方法签名的默认方法,此时子类任然需要实现这个抽象方法,而不能使用接口的默认方法作为实现!
- 如果一个类实现的接口之间存在继承关系,那么该类可以手动选择调用最低级别接口的的默认实现,但是手动选择调用其他级别接口的的默认实现。
- Java强大的编译机制帮助我们解决了菱形继承问题,我们自己不需要解决。什么是菱形继承问题:即有个接口A,有个默认方法a(),此时子接口B、C继承了A接口,随后实现类D同时实现了B、C接口,此时在D中调用a()方法不会有问题,但是c++就有问题!
- 如果接口B或者C复写了方法a(),那么在D中调用的a()方法,就是B或者C的a()方法。
- 如果接口B和C都复写了默认方法a(),那么就会出现冲突。
- 如果接口B或者C复写了默认方法a(),但是变成了抽象方法,那么那么在D中必须实现该方法!
案例:
/** * @author lx */ public class InterfaceTest { /** * 测试1 本类重写的方法优先级最高。 */ static class InterfaceTest1 extends InterfaceClass4 implements Interface3, Interface { public static void main(String[] args) { InterfaceTest1 interfaceTest = new InterfaceTest1(); System.out.println(interfaceTest.handle()); } /** * 自己重写的方法优先级最高 */ @Override public int handle() { return 5; } } /** * 测试2 父类中声明的方法的优先级高于任何接口中声明的默认方法的优先级。 */ static class InterfaceTest2 extends InterfaceClass4 implements Interface3, Interface { public static void main(String[] args) { InterfaceTest2 interfaceTest = new InterfaceTest2(); //继承了InterfaceClass4父类,因此父类中相同方法签名的方法优先级最高 System.out.println(interfaceTest.handle()); } } /** * 测试3 最具体实现的默认方法的接口优先级最高,即最底层的子接口 */ static class InterfaceTest3 implements Interface3, Interface, Interface0 { public static void main(String[] args) { InterfaceTest3 interfaceTest = new InterfaceTest3(); //Interface3接口继承了Interface借口路,因此Interface3的默认方法优先级最高 System.out.println(interfaceTest.handle()); } } /** * 测试4 上面的方式无法判断,并且编译不通过,只能手动指定 */ static class InterfaceTest4 implements Interface1, Interface2 { public static void main(String[] args) { InterfaceTest4 interfaceTest = new InterfaceTest4(); //Interface3接口继承了Interface借口路,因此Interface3的默认方法优先级最高 System.out.println(interfaceTest.handle()); } /** * 通过 Interface1.super.handle();指定调用某个接口的默认方法 */ @Override public int handle() { return Interface1.super.handle(); } } /** * 菱形继承问题,编译通过,运行正常 * 这实际上就是c++的菱形继承问题,但是Java中帮助我们解决了,我们自己不需要解决 * 它会自动递归向上查找,找到Interface4和Interface5的共同父接口Interface,然后调用里面的方法,而c++则会抛出异常 */ static class InterfaceTest5 implements Interface4, Interface5 { public static void main(String[] args) { InterfaceTest5 interfaceTest = new InterfaceTest5(); //Interface3接口继承了Interface借口路,因此Interface3的默认方法优先级最高 System.out.println(interfaceTest.handle()); } } /** * 注意1 如果一个类实现了抽象类和接口,并且接口中具有和抽象类中的抽象方法同样方法签名的默认方法 * 此时子类仍然需要实现这个抽象方法,而不能使用接口的默认方法作为实现,否则编译不通过! */ static class InterfaceTestt1 extends InterfaceClass5 implements Interface3, Interface { public static void main(String[] args) { InterfaceTestt1 interfaceTest = new InterfaceTestt1(); //继承了InterfaceClass4父类,因此父类中相同方法签名的方法优先级最高 System.out.println(interfaceTest.handle()); } //仍然需要实现这个抽象方法 @Override public int handle() { return 0; } } /** * 注意2 2 如果一个类实现的接口之间存在继承关系 * 那么该类可以手动选择调用最低级别接口的的默认实现,但是手动选择调用其他级别接口的的默认实现。 */ static class InterfaceTestt2 implements Interface3, Interface, Interface0 { public static void main(String[] args) { InterfaceTestt2 interfaceTest = new InterfaceTestt2(); //继承了InterfaceClass4父类,因此父类中相同方法签名的方法优先级最高 System.out.println(interfaceTest.handle()); } /** * 可以手动选择调用Interface3的方法 * 不能可以手动选择调用Interface和Interface0的方法 */ @Override public int handle() { return Interface3.super.handle(); // return Interface.super.handle(); // return Interface0.super.handle(); } } } interface Interface0 { default int handle() { return -1; } } interface Interface extends Interface0 { @Override default int handle() { return 0; } } interface Interface2 { default int handle() { return 2; } } interface Interface1 { default int handle() { return 1; } } interface Interface3 extends Interface { @Override default int handle() { return 3; } } interface Interface4 extends Interface { } interface Interface5 extends Interface { } class InterfaceClass4 { public int handle() { return 4; } } abstract class InterfaceClass5 { abstract int handle(); }
6 Lambda的复合
由于lambda相当于一个函数或者行为,因此Java8允许把多个简单的Lambda复合成复杂的表达式,将简单的函数复合成为复杂的函数,这其中就用到了上面的默认方法和静态方法。
6.1 Comparator比较器复合
返回 方法 描述 static < T,U extends Comparable<? super U >> Comparator< T > comparing(Function< ? super T,? extends U > keyExtractor) 使用指定的keyExtractor提取需要比较的键,返回一个自然排序比较器。 static < T,U > Comparator< T > comparing(Function< ? super T,? extends U > keyExtractor, Comparator< ? super U > keyComparator) 使用指定的keyExtractor提取需要比较的键,返回一个指定排序的比较器。 default Comparator< T > reversed() 返回一个与调用比较器相反的比较器 static < T extends Comparable<? super T >> Comparator reverseOrder() 返回一个与 自然排序相反的比较器。 default Comparator< T > thenComparing(Comparator< ? super T > other) 当调用比较器比较两个对象相等时使用另一个参数副比较器进行比较。 default < U extends Comparable<? super U >> Comparator< T > thenComparing(Function< ? super T,? extends U > keyExtractor) 当调用比较器比较两个对象相等时使用另一个参数副比较器进行比较。使用指定的keyExtractor提取需要比较的键 Java8开始,支持Comparator比较器的复合,添加了许多静态方法和默认方法。主要有reversed方法,该方法用于返回一个与调用比较器相反排序顺序的比较器,以及thenComparing方法,该方法类似于复合比较器,调用方法的比较器作为主要比较器,参数比较器作为副比较器,如果主比较器比较相等,那么使用副比较器比较排序!
/** * @author lx */ public class CompositeTest { ArrayList<User> userArrayList = new ArrayList<>(); @Before public void beforeTest() { User user4 = new User(20, "da"); User user1 = new User(25, "张三"); User user3 = new User(20, "张小三"); User user2 = new User(25, "tom"); userArrayList.add(user1); userArrayList.add(user2); userArrayList.add(user3); userArrayList.add(user4); } /** * 比较器复合 */ @Test public void test() { /*1 首先是通过user的age比较大小并顺序排序,我们使用JDK8提供的comparingInt静态方法*/ Comparator<User> userComparator = Comparator.comparingInt(User::getAge); userArrayList.sort(userComparator); System.out.println("age顺序:" + userArrayList); //上面的comparingInt方法就相当于下面的两个表达式 //实际上这里的ToIntFunction用来提取需要比较的两个对象的的属性 //只不过这两个操作被封装到了comparingInt方法中,因此我们只需要传递一个ToIntFunction的函数即可 ToIntFunction<User> toIntFunction = value -> value.getAge(); Comparator<User> userComparator1 = (c1, c2) -> Integer.compare(toIntFunction.applyAsInt(c1), toIntFunction.applyAsInt(c2)); /* * 2 现在如果我们需要进行逆序排序 * 此时我们只需要将以前的比较器顺序反过来就行了 * 比较器调用reversed方法,返回的比较器将会使用和调用比较器相反的顺序。 */ Comparator<User> reversed = userComparator.reversed(); userArrayList.sort(reversed); System.out.println("age逆序:" + userArrayList); /* * 3 有时候我们需要进行多个参数的比较 * 现在我们需要在age相等的基础上再比较姓名长度并顺序排序 * 此时我们可以调用thenComparing方法,传递一个比较器这表示将两个比较器复合 * 调用方法的比较器被看作主要比较器,参数的比较器看作副比较器 * 或者更进一步,我们类似于第一个获取比较器的方式,传递一个能共提取比较的键的ToIntFunction */ Comparator<User> userComparator2 = reversed.thenComparingInt(o -> o.getName().length()); userArrayList.sort(userComparator2); System.out.println("age逆序-name长度顺序:" + userArrayList); } public class User { private int age; private String name; public int getAge() { return age; } public void setAge(int age) { this.age = age; } public String getName() { return name; } public void setName(String name) { this.name = name; } public User() { } public User(int age, String name) { this.age = age; this.name = name; } @Override public String toString() { return "User{" + "age=" + age + ", name='" + name + '\'' + '}'; } } }
6.2 Function函数复合
返回 方法 描述 default < V > Function< T,V > andThen(Function< ? super R,? extends V > after) 返回一个复合函数,首先将该函数应用于其输入,然后将 after函数应用于结果。 default < V > Function< V,R > compose(Function< ? super V,? extends T > before) 返回一个复合函数,首先将 before函数应用于其输入,然后将此函数应用于结果。 static < T > Function< T,T > identity() 返回一个总是返回其输入参数的函数。 andThen和compose的运算顺序是相反的。andThen方法中,先执行调用者函数的计算,然后将结果作为参数传给after参数函数,最后执行after参数函数的计算。compose方法中,先执行before参数函数的计算,然后将结果作为参数传给调用者函数,最后执行调用者函数的计算。
/** * 函数复合 andThen计算 */ @Test public void andThen() { User user1 = new User(20, "小花"); //这个函数根据传入的user获取age Function<User, Integer> f1 = User::getAge; //这个函数根据传入的int值创建一个user Function<Integer, User> f2 = integer -> new User(integer++); //将函数f1和f2复合,即获取传入user的age创建一个新的user,获得新的函数f3 //可以看到f3的参数就是f1的参数,f3的返回类型,就是f2的返回类型,相当于将f1和f2串联了起来 //先执行f1,然后将结果传给f2,最后执行f2 Function<User, User> f3 = f1.andThen(f2); System.out.println(f3.apply(user1)); } /** * 函数复合 compose计算 */ @Test public void compose() { //这个函数根据传入的user获取age Function<User, Integer> f1 = User::getAge; //这个函数根据传入的int值创建一个user Function<Integer, User> f2 = integer -> new User(integer++); //将函数f1和f2复合,即获取传入user的age创建一个新的user,获得新的函数f3 //可以看到f3的参数就是f2的参数,f3的返回类型就是f1的返回类型,相当于将f2和f1串联了起来 //先执行f2,然后将结果传给f1,最后执行f1 Function<Integer, Integer> f3 = f1.compose(f2); System.out.println(f3.apply(10)); }
6.3 Consumer消费复合
返回 方法 描述 default Consumer< T > andThen(Consumer< ? super T > after) 返回一个复合的 Consumer ,按顺序执行该操作,然后执行 after操作。 Consumer的andThen方法相当于按照顺序对最开始传递的参数进行一系列计算。先在调用者里面执行参数计算,然后将参数传给after,最后执行在after里面执行参数计算。
/** * 消费复合 Consumer的andThen计算 */ @Test public void andThenCom() { User user = new User(20, "小花"); //一个Consumer设置名字 Consumer<User> c1 = o -> o.setName("花小"); //一个Consumer设置年龄 Consumer<User> c2 = o -> o.setAge(10); //组合 //先在调用者里面执行参数计算,然后将参数传给after,最后执行在after里面执行参数计算。 Consumer<User> c3 = c1.andThen(c2); c3.accept(user); System.out.println(user); //链式编程写法 c1.andThen(o -> o.setAge(10)).accept(user); }
6.4 Predicate断言复合
返回 方法 描述 default Predicate< T > and(Predicate< ? super T > other) 返回一个组合断言,表示两个断言的&&连接 default Predicate< T > negate() 返回一个非断言,表示目前断言的!关系 default Predicate< T > or(Predicate< ? super T > other) 返回一个组合断言,表示两个断言的||连接 断言型接口Predicate内部提供了and、negate、or默认方法,用于“&&(与)、!(非)、||(或)”的方式连接两个断言!
调用断言的结果将会先被计算,随后与参数断言的结果进行比较,调用断言在前,参数断言在后!/** * 断言复合 */ @Test public void predicate() { User user = new User(20, "小花"); //一个断言用于判断年龄是否大于20 Predicate<User> p1 = (User u) -> u.getAge() >= 20; //一个断言用于判断姓名长度是否大于等于3 Predicate<User> p2 = (User u) -> u.getName().length() >= 3; //组合 //&& Predicate<User> and = p1.and(p2); System.out.println("年龄大于等于20并且姓名长度大于等于3:---" + and.test(user)); //|| Predicate<User> or = p1.or(p2); System.out.println("年龄大于等于20或者姓名长度大于等于3:---" + or.test(user)); //! Predicate<User> negate = p1.negate(); System.out.println("年龄小于20:---" + negate.test(user)); User user2 = new User(19, "花花花"); //多重组合 //年龄大于等于20并且年龄大于3,或者姓名等于花花花 System.out.println(p1.and((User u) -> u.getName().length() > 3).or((User u) -> "花花花".equals(u.getName())).test(user2)); //年龄大于等于20并且年龄大于等于3,或者姓名等于花花花 System.out.println(p1.and((User u) -> u.getName().length() >= 3).or((User u) -> "花花花".equals(u.getName())).test(user2)); }
7 Lambda与匿名内部类
在Java8引入lambda之前,我们使用匿名内部类来完成“避免”类的创建,之后我们使用lambda来代替函数式接口的匿名内部类以及对象的创建,它们之间从上层特性到底层原理都有很多的不同:
- 关键字 this
- 匿名内部类中的 this 就是代表当前匿名类对象。
- 在lambda表达中引用this关键字,和在lambda外部引用的意义一样。因为lambda不是内部类对象,那么在lambda内部引用this也就和内部类没什么关系了。
/** * @author lx */ public class ThisTest { public static void main(String[] args) { ThisTest thisTest = new ThisTest(); System.out.println("thisTest对象:"+thisTest); thisTest.thisTest(); } public void thisTest() { System.out.println("----------------"); ThisTest th=this; System.out.println("当前this对象:"+this); System.out.println("----------------"); Comparator<Integer> comparator = new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { System.out.println("匿名内部类的this:"+this); return o1.compareTo(o2); } }; comparator.compare(1, 2); System.out.println("----------------"); Comparator<Integer> integerComparator = (x, y) -> { System.out.println("lambda的this:"+this); System.out.println(th==this); return x.compareTo(y); }; integerComparator.compare(1, 2); } }
应用范围:
- 匿名内部类可以为任意接口创建实例。不管接口中包含多少个抽象方法,只要匿名内部类实现所有的抽象方法即可,也可以为抽象类甚至普通类创建实例。
- lambda表达式只能为函数式接口创建实例。
- 方法调用
- 匿名内部类实现抽象方法的方法体中允许调用接口中定义的默认方法;但Lambda表达式的代码块中不允许调用接口中定义的默认方法。
- 匿名内部类内调用与外部类有相同签名的方法时,实际调用的是该匿名内部类实例的方法。而lambda调用与外部类有相同签名的方法是,实际调用的是外部类实例的方法。
- 底层实现
- 虽然匿名内部类的使用避免了我们手动创建类,但实际上仍然会被编译器编译成一个.class文件,相当于是帮我们了一个创建一个独立的类,文件命名方式为:主类名+ + + + + +++(1.2.3…)。而在程序启动的时候,JVM会对用到的全部类进行加载、验证、准备、解析、初始化等操作,匿名内部类生成的类文件也不例外,因此大量的内部类文件的创建将会影响应用程序启动执行的性能。
- 对于lambda表达式,Java编译器使用Java7引入的 invokedynamic 字节码指令(为支持动态类型语言新增的指令)。invokedynamic指令不会在编译时就进行类型检查而产生新的类文件,而是将lambda表达式的字节码类型检查转换操作推迟到了第一次调用时,相当于一个调用点,仅在lambda表达式被首次调用的时候(执行到invokedynamic调用点),才会通过反射创建一个匿名的lambda实现类以及对象,之后的调用也都会跳过这一步骤,没有了程序启动时就进行的类加载过程,而是第一次用到的时候才会进行相应的类动态创建工作,自然提升了性能。
/** * @author lx */ public class LambdaInvoke { public static void main(String[] args) { classTest(new Comparator<Integer>() { @Override public int compare(Integer o1, Integer o2) { return o1.compareTo(o2); } }); lambdaTest((Comparator<Integer>) Integer::compareTo); } public static void classTest(Comparator comparator) {} public static void lambdaTest(Comparator comparator) {} }
使用Javap -v 查看class文件的字节码,可以发现匿名内部类对象的创建工作就是使用了一般的new指令而已,这说明这个匿名类在程序启动的时候就被加载进来了,而lambda表达式则使用了invokedynamic指令,对应的类以及对象会在执行时被动态的加载。
8 总结
lambda为Java这种面向对象的语言带来了函数式编程的写法,改变了Java只能面像对象的局限性,某些情况下能够极大地减少代码量。Java引入lambda的目的并不是为了完全取代面向对象编程,而是为了方便我们使用混合开发方式,在合适的情况下采用合理的编程方式,能够有效的提高开发效率!
lambda的另一个重要应用就是同样在Java8新增的Stream API中,几乎都可以使用函数式接口与lambda作为参数完成功能强大的流式编程!
Stream API中才是lambda大展身手的地方!
相关文章:
Stream:Java8—两万字的Stream流的详细介绍与应用案例
如有需要交流,或者文章有误,请直接留言。另外希望点赞、收藏、关注,我将不间断更新各种Java学习博客!
-
Java值传递和引用传递详细说明
2020-07-14 15:53:17学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运用却解释不出原理,而且坊间讨论的话题又是充满争议:有的论坛帖子说Java只有值传递,...本文旨在用最通俗的语言讲述最枯燥的基本知识
学过Java基础的人都知道:值传递和引用传递是初次接触Java时的一个难点,有时候记得了语法却记不得怎么实际运用,有时候会的了运用却解释不出原理,而且坊间讨论的话题又是充满争议:有的论坛帖子说Java只有值传递,有的博客说两者皆有;这让人有点摸不着头脑,下面我们就这个话题做一些探讨,对书籍、对论坛博客的说法,做一次考证,以得出信得过的答案。
其实,对于值传递和引用传递的语法和运用,百度一下,就能出来可观的解释和例子数目,或许你看一下例子好像就懂,但是当你参加面试,做一道这个知识点的笔试题时感觉自己会,胸有成熟的写了答案,却发现是错的,或者是你根本不会做。
是什么原因?
那是因为你对知识点没有了解透彻,只知道其皮毛。要熟读一个语法很简单,要理解一行代码也不难,但是能把学过的知识融会贯通,串联起来理解,那就是非常难了,在此,关于值传递和引用传递,小编会从以前学过的基础知识开始,从内存模型开始,一步步的引出值传递和引用传递的本质原理,故篇幅较长,知识点较多,望读者多有包涵。
1. 形参与实参
我们先来重温一组语法:
-
形参:方法被调用时需要传递进来的参数,如:func(int a)中的a,它只有在func被调用期间a才有意义,也就是会被分配内存空间,在方法func执行完成后,a就会被销毁释放空间,也就是不存在了
-
实参:方法被调用时是传入的实际值,它在方法被调用前就已经被初始化并且在方法被调用时传入。
举个栗子:
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的数据类型有哪些?-
基本类型:编程语言中内置的最小粒度的数据类型。它包括四大类八种类型:
4种整数类型:byte、short、int、long
2种浮点数类型:float、double
1种字符类型:char
1种布尔类型:boolean-
引用类型:引用也叫句柄,引用类型,是编程语言中定义的在句柄中存放着实际内容所在地址的地址值的一种数据形式。它主要包括:
类
接口
数组有了数据类型,JVM对程序数据的管理就规范化了,不同的数据类型,它的存储形式和位置是不一样的,要想知道JVM是怎么存储各种类型的数据,就得先了解JVM的内存划分以及每部分的职能。
3.JVM内存的划分及职能
Java语言本身是不能操作内存的,它的一切都是交给JVM来管理和控制的,因此Java内存区域的划分也就是JVM的区域划分,在说JVM的内存划分之前,我们先来看一下Java程序的执行过程,如下图:
有图可以看出:Java代码被编译器编译成字节码之后,JVM开辟一片内存空间(也叫运行时数据区),通过类加载器加到到运行时数据区来存储程序执行期间需要用到的数据和相关信息,在这个数据区中,它由以下几部分组成:1. 虚拟机栈
2. 堆
3. 程序计数器
4. 方法区
5. 本地方法栈
我们接着来了解一下每部分的原理以及具体用来存储程序执行过程中的哪些数据。
1. 虚拟机栈
虚拟机栈是Java方法执行的内存模型,栈中存放着栈帧,每个栈帧分别对应一个被调用的方法,方法的调用过程对应栈帧在虚拟机中入栈到出栈的过程。
栈是线程私有的,也就是线程之间的栈是隔离的;当程序中某个线程开始执行一个方法时就会相应的创建一个栈帧并且入栈(位于栈顶),在方法结束后,栈帧出栈。
下图表示了一个Java栈的模型以及栈帧的组成:
栈帧:是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈的栈元素。每个栈帧中包括:
-
局部变量表:用来存储方法中的局部变量(非静态变量、函数形参)。当变量为基本数据类型时,直接存储值,当变量为引用类型时,存储的是指向具体对象的引用。
-
操作数栈:Java虚拟机的解释执行引擎被称为"基于栈的执行引擎",其中所指的栈就是指操作数栈。
-
指向运行时常量池的引用:存储程序执行时可能用到常量的引用。
-
方法返回地址:存储方法执行完成后的返回地址。
2. 堆:
堆是用来存储对象本身和数组的,在JVM中只有一个堆,因此,堆是被所有线程共享的。
3. 方法区:
方法区是一块所有线程共享的内存逻辑区域,在JVM中只有一个方法区,用来存储一些线程可共享的内容,它是线程安全的,多个线程同时访问方法区中同一个内容时,只能有一个线程装载该数据,其它线程只能等待。
方法区可存储的内容有:类的全路径名、类的直接超类的权全限定名、类的访问修饰符、类的类型(类或接口)、类的直接接口全限定名的有序列表、常量池(字段,方法信息,静态变量,类型引用(class))等。
4. 本地方法栈:
本地方法栈的功能和虚拟机栈是基本一致的,并且也是线程私有的,它们的区别在于虚拟机栈是为执行Java方法服务的,而本地方法栈是为执行本地方法服务的。
有人会疑惑:什么是本地方法?为什么Java还要调用本地方法?
5. 程序计数器:
线程私有的。
记录着当前线程所执行的字节码的行号指示器,在程序运行过程中,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,分支、循环、异常处理、线程恢复等基础功能都需要依赖计数器完成。
4. 数据如何在内存中存储?
从上面程序运行图我们可以看到,JVM在程序运行时的内存分配有三个地方:
-
堆
-
栈
-
静态方法区
-
常量区
相应地,每个存储区域都有自己的内存分配策略:
-
堆式:
-
栈式
-
静态
我们已经知道:Java中的数据类型有基本数据类型和引用数据类型,那么这些数据的存储都使用哪一种策略呢?
这里要分以下的情况进行探究:1. 基本数据类型的存储:
-
A. 基本数据类型的局部变量
-
B. 基本数据类型的成员变量
-
C. 基本数据类型的静态变量
2. 引用数据类型的存储
1. 基本数据类型的存储
我们分别来研究一下:
A.基本数据类型的局部变量
-
定义基本数据类型的局部变量以及数据都是直接存储在内存中的栈上,也就是前面说到的“虚拟机栈”,数据本身的值就是存储在栈空间里面。
如上图,在方法内定义的变量直接存储在栈中,如
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. 基本数据类型的成员变量
成员变量:顾名思义,就是在类体中定义的变量。
看下图:我们看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方法所在的栈帧中
如图:
而当执行到valueCrossTest()方法时,JVM也为其往虚拟机栈中压入一个栈,即为当前栈帧,用来存放valueCrossTest()中的局部变量等信息,因此age和weight是躺着valueCrossTest方法所在的栈帧中,而他们的值是从a和w的值copy了一份副本而得,如图:因而可以a和age、w和weight对应的内容是不一致的,所以当在方法内重新赋值时,实际流程如图:
也就是说,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对象的真实地址,如图:
当执行到PersonCrossTest()方法时,因为方法内有这么一行代码:1person=new Person();
JVM需要在堆内另外开辟一块内存来存储new Person(),假如地址为“xo3333”,那此时形参person指向了这个地址,假如真的是引用传递,那么由上面讲到:引用传递中形参实参指向同一个对象,形参的操作会改变实参对象的改变。
可以推出:实参也应该指向了新创建的person对象的地址,所以在执行PersonCrossTest()结束之后,最终输出的应该是后面创建的对象内容。
然而实际上,最终的输出结果却跟我们推测的不一样,最终输出的仍然是一开始创建的对象的内容。
由此可见:引用传递,在Java中并不存在。
但是有人会疑问:为什么第一个例子中,在方法内修改了形参的内容,会导致原始对象的内容发生改变呢?
这是因为:无论是基本类型和是引用类型,在实参传入形参时,都是值传递,也就是说传递的都是一个副本,而不是内容本身。
有图可以看出,方法内的形参person和实参p并无实质关联,它只是由p处copy了一份指向对象的地址,此时:
p和person都是指向同一个对象。
因此在第一个例子中,对形参p的操作,会影响到实参对应的对象内容。而在第二个例子中,当执行到new Person()之后,JVM在堆内开辟一块空间存储新对象,并且把person改成指向新对象的地址,此时:
p依旧是指向旧的对象,person指向新对象的地址。
所以此时对person的操作,实际上是对新对象的操作,于实参p中对应的对象毫无关系。
结语
因此可见:在Java中所有的参数传递,不管基本类型还是引用类型,都是值传递,或者说是副本传递。
只是在传递过程中:如果是对基本数据类型的数据进行操作,由于原始内容和副本都是存储实际值,并且是在不同的栈区,因此形参的操作,不影响原始内容。
如果是对引用类型的数据进行操作,分两种情况,一种是形参和实参保持指向同一个对象地址,则形参的操作,会影响实参指向的对象的内容。一种是形参被改动指向新的对象地址(如重新赋值引用),则形参的操作,不会影响实参指向的对象的内容。
原文转至:https://blog.csdn.net/bntx2jsqfehy7/article/details/83508006
-
-
Matlab中数组元素引用——三种方法
2018-07-19 12:44:251.Matlab中数组元素引用有三种方法 1.下标法(subscripts)2.索引法(index)3.布尔法(Boolean) 注意:在使用这三种方法之前,大家头脑一定要清晰 -
Python中模块之间变量引用的方法说明
2016-07-01 23:30:40http://blog.csdn.net/andoring/article/details/6589604 -
vue学习笔记五:在vue项目里面使用引入公共方法
2018-04-25 10:15:16今天早上来到公司,没事看了一下别人的博客,然后试了...建立好之后,在main.js里面引入这个公共方法 最后是调用这个公共方法 测试一下,我在公共方法里面写了一个简单的一段代码如下: export default{ ... -
Java8 方法引用-Method References
2017-06-22 00:13:11前言怎样更好的引入Java8中的方法引用呢? 我们首先看一下一个简单的例子: Consumer<String> con=(str)->System.out.println(str); con.accept("我是消费型接口!");这时候你就会知道这行代码输出的是:我是消费型... -
C#引用类型和值类型在堆、栈中的存储
2020-10-12 16:14:10一个数据项需要多大的内存、存储在什么地方、以及如何存储都依赖于该数据项的类型。 运行中的程序使用两个内存区域来存储数据:栈和堆。 1、栈 栈是一个内存数组,是一个LIFO(last-in first-out,后进先出)的... -
『Java面经』ThreadLocal 实现原理是什么 & 有哪些引用类型及使用场景?
2021-10-17 15:56:51文章目录1、作用2、保存步骤3、原理 / 内存泄漏3.1 数据结构3.2 设计原理3.3 value如何清理3.4 总结4、四种引用类型5、微信关注『方糖算法』 1、作用 ThreadLocal 在多线程环境中,安全的保存线程本地变量,同一... -
idea中maven加载包完成后,项目中不能自动正常引用包中的方法
2018-10-15 10:05:061、出现的原因:工程出现项目混乱,一个项目中含有两个子模块,其中一个子模块中的pom.xml没有添加父模块的引用,一个添加了父模块的引用,而父pom.xml中同时写了这两个子模块,导致项目出现目录混乱,包不能正常... -
【Latex】用Bibtex来引用文献的方法及说明
2019-08-16 17:14:40参考了这篇文章,本文针对文章进行一些补充说明。 环境: TeXLive2017 TeXstudio2.12.10 提示1:每次改完.tex文件中的错误之后,都要删掉.aux和.bbl两个文件,然后再重新编译。 提示2:修改.bbl文件之后,... -
iOS之深入解析如何检测“循环引用”
2022-04-01 18:25:57一、前言 Objective-C 使用引用计数作为 iPhone 应用的内存...虽然 Objective-C 通过引入弱引用技术,让开发者可以尽可能地规避这个问题,但在引用层级过深,引用路径不那么直观的情况下,即使是经验丰富的工程师,也 -
GC回收之一:判断对象存活算法、四种引用、回收方法区
2018-05-01 21:51:17判断对象是否存活的算法:1、引用计数算法 给对象添加一个引用计数器,当有一个地方引用它时,计数器值就加1;当引用失效时,计数器就减1;任何时候计数器都为0的对象就是不可能再被使用的。引用计数器算法... -
程序的依赖和引用是什么?
2019-09-30 22:08:02在厨师这个类里面有个叫“降龙十八炒”的方法(function),于是我可以调用厨师这个类当中的这个方法来完成这个工作。 什么叫引用 在VS界面的引用里面可以看到很多dll文件,这就是程序的引用。程序的引用简单理解... -
jdk8新特性 forEach方法和方法引用
2018-11-29 20:30:33先说明,jdk8增加了一个包java.util.function,里面存放的都是新增的函数式接口,方便用lambda表达式重写其抽象方法 下面列举三个常见的函数式接口,下行是其抽象方法 Consumer<T>代表了接受一个输入... -
Python中py文件引用另一个py文件变量的方法
2021-04-27 05:41:00Python中py文件引用另一个py文件变量的方法最近自己初学Python,在编程是遇到一个问题就是,怎样在一个py文件中使用另一个py文件中变量,问题如下:demo1代码import requestsr = requests.get(... -
超级简单在Latex中用Bibtex引用参考文献方法
2019-01-21 11:18:34标题超级简单在Latex中用Bibtex引用参考文献方法 最近自己在试着用LaTeX写文章,发现引用参考文献出现许多问题,自己在网上看到方法后,测试过后,自己想做个总结。 首先说明的是,自己用的是WinEdt7.0写文章, ... -
在LaTeX中如何引用参考文献
2020-10-13 18:09:56先在文章文章末尾写好需要插入的参考文献,逐一写出,例如: \begin{thebibliography}{99} \bibitem{ref1}郭莉莉,白国君,尹泽成,魏惠芳. “互联网+”背景下沈阳智慧交通系统发展对策建议[A]. 中共沈阳市委、沈阳... -
课设——论文中的为什么要引用参考文献以及如何引用
2019-03-19 21:06:55一、 引用参考文献的目的: 尊重作者劳动成果。 利用他人的实验结论支撑自己的结论,减少工作量,否则证明一个数学题应该从1+1=2开始证明,才足够严谨。对别人已经研究过下过结论的内容,我们不需要再做研究,只... -
Java:强引用,软引用,弱引用和虚引用
2019-01-02 16:56:19在JDK1.2以前的版本中,当一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及状态,程序才能使用它。这就像在商店购买了某样物品后,如果有用就一直保留它,否则就把它扔到垃圾... -
excel如何在双引号里面引用变量
2015-06-15 18:45:441 说明 注意&的用法,"copy"输入copy字符串;"“"输入字符“;A1输入A1单元格值;"\"输入字符\;B1输入B1单元格值;"”"输入字符”;" "输入空格;"“"输入字符“;C1输入C1单元格值;"”"输入字符” 要... -
JNI笔记 : 在JNI中使用引用
2018-07-08 00:03:23在Java中,通常分为四种引用类型,分别是:强引用、软引用、弱引用以及虚引用。对于一个Java对象来说,当被强引用所引用时,只要该对象可达,就不会被GC回收;当被软引用所引用时,当内存不足时才有可能会被回收;当... -
论文参考文献尾注引用方法
2019-04-28 10:25:36论文参考文献引 使用工具 microsoft office 2013 一、设置尾注的格式 这一步为设置尾注的格式,可以使你的尾注...在这一步中,为论文中所有引用的部分添加参考文献。 1.首先,选择中你引用的一段话,如图: 2.然... -
java 怎么去调用jar包里面的方法
2019-04-29 18:42:16首先在eclipse中创建一个java... 工程和jar包都准备好后,就可以使用jar包里面的方法了。 使用jar包里面的方法通常有三个步骤: 1、先将jar包添加到构建路径中 2、在类中导入jar包 3、调用jar包的方法。 ... -
C++ 引用的本质是什么?
2017-04-07 23:45:16C++中的引用本质上是 一种被限制的指针。由于引用是被限制的指针,所以...在使用高级语言的层面上,是没有提供访问引用的方法的。并且引用创建时必需初始化,创建后还不能修改。下面是找到的相关资料,来证明以上结论。 -
C++中 引用符&,以int&举例说明
2017-11-26 10:52:46变量的引用就是变量的别名,讲的通俗一点就是另外一个名字,比如:“张三这个人在家里,老爸老妈叫他三娃子,那么这个三娃子就是指张三这个人,如果叫张三去做某事,就是叫三娃子去做某事,这两个名字指的是同一个人... -
静态方法访问非静态方法引发的错误:不能对类型 XXX中的非静态方法 xxx()进行静态引用
2019-07-02 16:24:41对于static修饰的方法而言,可以使用类来直接调用该方法,如果在static修饰的方法中使用this关键字,则这个关键字就无法指向合适的对象。所以,static修饰的方法中不能使用this 引用。由于static修饰的方法不能使用... -
Excel基础知识(1):公式中相对引用、绝对引用的区别
2020-11-18 12:52:15先来看绝对引用。如下图所示,公式“=C15/$C$23”中的分母,在行23、列C前面加了$,这就表明对单元格C23做了固定,无论怎么拖拉、复制这个公式所在单元格,分母C23都不会改变。分子是C15是相对引用,所以当将单元格... -
【WPS】论文添加引用和参考文献简明方法 (图文详解)
2020-04-21 20:46:51对于本科生的日常作业、报告等,经常会用Office Word 或 WPS Word 来编辑和整理。本文将记录一下用 WPS Word 来为论文添加引用和参考文献的方法。