范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

深入理解Java泛型

  #头条创作挑战赛#什么是Java泛型
  大家好,我是呼噜噜,Java 泛型(generics)是 Jdk 5 中引入的一个新特性, 泛型提供了编译时类型安全检测机制, 该机制允许程序员在编译时检测到非法的类型。
  比如 ArrayList list= new ArrayList()这行代码就指明了该 ArrayList 对象只能 存储String类型,如果传入其他类型的对象就会报错。 让我们时光回退到Jdk5的版本,那时ArrayList内部其实就是一个Object[] 数组,配合存储一个当前分配的长度,就可以充当"可变数组": public class ArrayList {     private Object[] array;     private int size;     public void add(Object e) {...}     public void remove(int index) {...}     public Object get(int index) {...} }
  我们来举个简单的例子, ArrayList list = new ArrayList(); list.add("test"); list.add(666);
  我们本意是用ArrayList来装String类型的值,但是突然混进去了Integer类型的值,由于ArrayList底层是Object数组,可以存储任意的对象,所以这个时候是没啥问题的,但我们不能只存不用啊,我们需要把值给拿出来使用,这个时候问题来了: for(Object item: list) {     System.out.println((String)item); }
  结果:
  Exception in thread "main" java.lang.ClassCastException: java.lang.Integer cannot be cast to java.lang.String
  由于我们需要String类型的值,我们需要把ArrayList的Object值强制转型,但是之前混进去了Integer ,虽然编译阶段通过了,但程序的运行结果会以崩溃结束,报 ClassCastException异常
  为了解决这个问题,在Jdk 5版本中就引入了泛型的概念,而引入泛型的很大一部分原因就是为了解决我们上述的问题,允许程序员在编译时检测到非法的类型。不是同类型的就不允许在一块存放,这样也避免了ClassCastException异常的出现,而且因为都是同一类型,也就没必要做强制类型转换了。 我们可以把ArrayList 变量参数化: public class ArrayList {     private T[] array;//我们 假设 ArrayList内部会有个T[] array       private int size;     public void add(T e) {...}     public void remove(int index) {...}     public T get(int index) {...} }
  其中 T叫类型参数  ,T可以是任何class类型,现在ArrayList我们可以如下使用: // 存储String的ArrayList ArrayList list = new ArrayList(); list.add(666);//编译器会在编译阶段发现问题,从而提醒开发者
  泛型其本质是参数化类型,也就是说数据类型 作为 参数,解决不确定具体对象类型的问题。 泛型的使用
  泛型一般有三种使用方式,分别为:泛型类、泛型接口、泛型方法,我们简单介绍一下泛型的使用 泛型类//此处T可以随便写为任意标识,常见的如T、E、K、V等形式的参数常用于表示泛型 //在实例化泛型类时,必须指定T的具体类型 public class Generic{      private T key;      public Generic(T key) {         this.key = key;     }      public T getKey(){         return key;     } }
  如何实例化泛型类: Generic genericInteger = new Generic(666); Generic genericStr = new Generic("hello");泛型接口//定义一个泛型接口 public interface Generator {     public T method(); }  //实现泛型接口,不指定类型 class GeneratorImpl implements Generator{     @Override     public T method() {         return null;     } }  //实现泛型接口,指定类型 class GeneratorImpl implements Generator{     @Override     public String method() {         return "hello";     } } 泛型方法public class GenericMethods {     public  void f(T x){         System.out.println(x.getClass().getName());     }     public static void main(String[] args) {         GenericMethods gm = new GenericMethods();         gm.f("啦啦啦");         gm.f(666);     } }
  结果: java.lang.String
  java.lang.Integer 泛型的底层实现机制ArrayList源码解析
  通过上文我们知道,为了让ArrayList存取各种数据类型的值,我们需要把 ArrayList模板化, 将变量的数据类型 给抽象出来,作为 类型参数 public class ArrayList {     private T[] array;// 我们以为ArrayList内部会有个T[] array     private int size;     public void add(T e) {...}     public void remove(int index) {...}     public T get(int index) {...} }
  但当我们查看Jdk8 的ArrayList源码,底层数组还是Object数组:transient Object[] elementData; 那ArrayList为什么还能进行类型约束和自动类型转换呢? 什么是泛型擦除
  我们再看一个经典的例子: public class genericTest {     public static void main(String [] args) {         String str="";         Integer param =null;          ArrayList l1 = new ArrayList();         l1.add("aaa");         str = l1.get(0);          ArrayList l2 = new ArrayList();         l2.add(666);         param = l2.get(0);           System.out.println(l1.getClass() == l2.getClass());              } }
  结果竟然是true,ArrayList.class 和 ArrayList.class 应该是不同的类型。通过getClass()方法获取他们的类的信息,竟然是一样的。我们来查看这个文件的class文件: public class genericTest {     public genericTest() {     }      public static void main(String[] var0) {         String var1 = "";         Integer var2 = null;         ArrayList var3 = new ArrayList();//泛型被擦擦了         var3.add("aaa");         var1 = (String)var3.get(0);         ArrayList var4 = new ArrayList();//泛型被擦擦了         var4.add(666);         var2 = (Integer)var4.get(0);         System.out.println(var3.getClass() == var4.getClass());     } }
  我们在对其反汇编一下: $ javap -c genericTest   :  ļ genericTest com.zj.demotest.test5.genericTest Compiled from "genericTest.java" public class com.zj.demotest.test5.genericTest {   public com.zj.demotest.test5.genericTest();     Code:        0: aload_0        1: invokespecial #1                  // Method java/lang/Object."":()V        4: return    public static void main(java.lang.String[]);     Code:        0: ldc           #2                  // String        2: astore_1        3: aconst_null        4: astore_2        5: new           #3                  // class java/util/ArrayList        8: dup        9: invokespecial #4                  // Method java/util/ArrayList."":()V       12: astore_3       13: aload_3       14: ldc           #5                  // String aaa       16: invokevirtual #6                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z       19: pop       20: aload_3       21: iconst_0       22: invokevirtual #7                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;       25: checkcast     #8                  // class java/lang/String       28: astore_1       29: new           #3                  // class java/util/ArrayList       32: dup       33: invokespecial #4                  // Method java/util/ArrayList."":()V       36: astore        4       38: aload         4       40: sipush        666       43: invokestatic  #9                  // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;       46: invokevirtual #6                  // Method java/util/ArrayList.add:(Ljava/lang/Object;)Z       49: pop       50: aload         4       52: iconst_0       53: invokevirtual #7                  // Method java/util/ArrayList.get:(I)Ljava/lang/Object;       56: checkcast     #10                 // class java/lang/Integer       59: astore_2       60: getstatic     #11                 // Field java/lang/System.out:Ljava/io/PrintStream;       63: aload_3       64: invokevirtual #12                 // Method java/lang/Object.getClass:()Ljava/lang/Class;       67: aload         4       69: invokevirtual #12                 // Method java/lang/Object.getClass:()Ljava/lang/Class;       72: if_acmpne     79       75: iconst_1       76: goto          80       79: iconst_0       80: invokevirtual #13                 // Method java/io/PrintStream.println:(Z)V       83: return } 看第16、46处,add进去的是原始类型Object; 看第22、53处,get方法获得也是Object类型,String、Integer类型被擦出,只保留原始类型Object。 看25、55处,checkcast指令是类型转换检查 ,在结合class文件var1 = (String)var3.get(0);``var2 = (Integer)var4.get(0);我们知晓编译器自动帮我们强制类型转换了,我们无需手动类型转换
  经过上面的种种现象,我们可以发现,在类加载的 编译阶段 ,泛型类型String和Integer都被擦除掉了,只剩下原始类型,这样他们类的信息都是Object,这样自然而然就相等了。这种机制就叫泛型擦除。
  我们需要了解一下类加载生命周期:
  详情见:https://mp.weixin.qq.com/s/v91bqRiKDWWgeNl1DIdaDQ
  泛型是和编译器的约定, 在编译期对代码进行检查的,由编译器负责解析,JVM并无识别的能力,一个类继承泛型后,当变量存入这个类的时候,编译器会对其进行类型安全检测,当从中取出数据时,编译器会根据与泛型的约定,会自动进行类型转换,无需我们手动强制类型转换。
  泛型类型参数化,并不意味这其对象类型是不确定的,相反它的对象类型 对于JVM来说,都是确定的,是Object或Object[]数组 泛型的边界
  来看一个经典的例子,我们想要实现一个ArrayList对象能够储存所有的泛型: ArrayList list = new ArrayList();
  但可以的是编译器提示报错:
  明明 String是Object类的子类,我们可以发现,泛型不存在继承、多态关系,泛型左右两边要一样 别担心,JDK提供了通配符?来应对这种场景,我们可以这样: ArrayList<?> list = new ArrayList(); list = new ArrayList();
  通配符<?>表示可以接收任意类型,此处?是类型实参,而不是类型形参。我们可以把它看做是String、Integer等所有类型的"父类"。是一种真实的类型。 通配符还有: 上边界限定通配符,如<? extends E>; 下边界通配符,如<? super E>; ?:无界通配符
  ?是开放限度最大的,可指向任意类型,但在对于其的存取上也是限制最大的: 入参和泛型相关的都不能使用, 除了null(禁止存入),比如ArrayList<?> list不可以添加任何类型,因为并不知道实际是哪种类型 返回值和泛型相关的都只能用Object接收 extends 上边界通配符//泛型的上限只能是该类型的类型及其子类,其中Number是Integer、Long、Float的父类    ArrayList<? extends Number> list = new ArrayList(); ArrayList<? extends Number> list2 = new ArrayList(); ArrayList<? extends Number> list3 = new ArrayList();  list.add(1);//报错,extends不允许存入  ArrayList longList = new ArrayList<>(); longList.add(1L); list = longList;//由于extends不允许存入,list只能重新指向longList  Number number = list.get(0);  // extends 取出来的元素(Integer,Long,Float)都可以转Number
  extends指向性被砍了一半,只能指向子类型和父类型,但方法使用上又适当放开了: 值得注意的是:这里的extends并不表示类的继承含义,只是表示泛型的范围关系 extends不允许存入,由于使用extends ,比如ArrayList<? extends Number> list可以接收Integer、Long、Float,但是泛型本质是保证两边类型确定,这样的话在程序运行期间,再存入数据,编译器可无法知晓数据的类型,所以只能禁止了。 但为什么ArrayList<? extends Number> list可以重新指向longList来变向地"存储"值,那是因为ArrayList longList = new ArrayList<>();这边的泛型已经约束两边的类型了,编译器知晓longList储存的数据都是Long类型 但extends 允许取出 ,取出来的元素可以往 边界类型 转 extends中可以指定多个范围,实行泛型类型检查约束时,会以最左边的为准。 super 下边界通配符//泛型的下限只能是该类型的类型及其父类,其中Number是Integer、Long、Float的父类    ArrayList<? super Integer> list = new ArrayList(); ArrayList<? super Integer> list2 = new ArrayList(); ArrayList<? super Integer> list3 = new ArrayList();//报错 ArrayList<? super Integer> list4 = new ArrayList();//报错  list2.add(123);//super可以存入,只能存Integer及其子类型元素 Object aa =  list2.get(0);//super可以取出,类型只能是Object
  super允许存入编辑类型及其子类型元素,但取出元素只能为Object类型 PECS原则
  泛型通配符的出现,是为了获得最大限度的灵活性。如果要用到通配符,需要结合业务考虑,《Effective Java》提出了: PECS(Producer Extends Consumer Super) 需要频繁往外读取内容(生产者Producer),适合用<? extends T> 需要频繁写值(消费者Consumer),适合用<? super T>:super 允许存入 子类型元素 ? 表示不确定的 java 类型,一般用于只接收任意类型,而不对其处理的情况 泛型是怎么擦除的
  Java 编译器通过如下方式实现擦除: 用 Object 或者界定类型替代泛型,产生的字节码中只包含了原始的类,接口和方法; 在恰当的位置插入强制转换代码来确保类型安全; 在继承了泛型类或接口的类中自动产生 桥接方法 来保留多态性。 擦除类定义中的无限制类型参数
  当类定义中的类型参数没有任何限制时,在类型擦除中直接被替换为Object,即形如和<?>的类型参数都被替换为Object
  擦除类定义中的有限制类型擦除
  当类定义中的类型参数存在限制(上下界)时,在类型擦除中替换为类型参数的上界或者下界, 形如 和<? extends Number>的类型参数被替换为Number, <? super Number>被替换为Object
  擦除方法定义中的类型参数
  擦除方法定义中的类型参数原则和擦除类定义中的类型参数是一样的,额外补充 擦除方法定义中的有限制类型参数的例子
  桥接方法和泛型的多态public class A{     public T get(T a){         //进行一些操作         return a;     } } public class B extends A{     @override     public String get(String a){         //进行一些操作         return a;     } }
  由于类型擦出机制的存在,按理说编译后的文件在翻译为java应如下所示: public class A{     public Object get(Object a){         //进行一些操作         return a;     } } public class B extends A{     @override     public String get(String a){         //进行一些操作         return a;     } }
  但是,我们可以发现@override意味着B对父类A中的get方法进行了重写,但是依上面的程序来看,只是重载,依然可以执行父类的方法,这和期望是不附的,也不符合java继承、多态的特性。 重写是子类对父类的允许访问的方法的实现过程进行重新编写, 返回值和形参都不能改变。 即外壳不变,核心重写!
  重载(overloading) 是在一个类里面,方法名字相同,而参数不同。返回类型可以相同也可以不同。
  为了解决这个问题,java在编译期间加入了桥接方法。编译后再翻译为java原文件其实是: public class A{     public Object get(Object a){         //进行一些操作         return a;     } } public class B extends A{     @override     public String get(String a){         //进行一些操作         return a;     }     //桥接方法!!!     public Object get(Object a){         return get((String)a)     } }
  桥接方法重写了父类相同的方法,并且桥接方法中,最终调用了期望的重写方法,并且桥接方法在调用目的方法时,参数被强制转换为指定的泛型类型。桥接方法搭起了父类和子类的桥梁。
  桥接方法是伴随泛型方法而生的,在继承关系中,如果某个子类覆盖了泛型方法,则编译器会在该子类 自动生成 桥接方法。所以我们实际使用泛型的过程中,无需担心桥接方法。
  ## 泛型擦除带来的限制与局限泛型不适用基本数据类型
  不能用类型参数代替基本类型(byte 、short 、int 、long、float 、 double、char、boolean) 比如,没有 Pair, 只 有 Pair。 其原因是泛型擦除,擦除之后只有原始类型Object, 而 Object 无法存储 double等基本类型的值。
  但Java同时有 自动拆装箱特性 ,可以将基本类型装箱成包装类型,这样就使用泛型了,通过中转, 即可在功能上实现"用基本类型实例化类型化参数"。
  数据类型
  封装类
  byte
  Byte
  short
  Short
  int
  Integer
  long
  Long
  float
  Float
  double
  Double
  char
  Character
  boolean
  Boolean 无法创建具体类型的泛型数组List[] l1 = new ArrayList[10];// Error List[] l2 = new ArrayList[10];// Error
  上文我们知晓ArrayList,底层仍旧采用Object[],Integer,String类型信息都被擦除
  借助无限定通配符?,可以创建泛型数组,但是涉及的操作都基本上与类型无关 List<?>[] l1 = new ArrayList<?>[10];
  如果想对数组进行复制操作的话,可以通过Arrays.copyOfRange()方法 public class TestArray {      public static void main(String[] args) {         Integer[] array = new Integer[]{2, 3, 1};         Integer[] arrNew = copy(array);     }      private static  E[] copy(E[] array) {         return Arrays.copyOfRange(array, 0, array.length);     }      }反射其实可以绕过泛型的限制
  由于我们知晓java是通过泛型擦除来实现泛型的,JVM只能识别原始类型Object,所以我们只需骗过编译器的校验即可,反射是程序运行时发生的,我们可以借助反射来波骚操作 List l1 = new ArrayList<>(); l1.add(111); //l1.add("骚气的我"); // 泛型会报错 try {     Method method = l1.getClass().getDeclaredMethod("add",Object.class);     method.invoke(l1,"骚气的我 又出现了"); } catch (NoSuchMethodException e) {     e.printStackTrace(); } catch (IllegalAccessException e) {     e.printStackTrace(); } catch (InvocationTargetException e) {     e.printStackTrace(); } for ( Object o: l1){     System.out.println(o); }
  结果: 111
  骚气的我 又出现了
  尾语
  如果你了解其他语言(例如 C++ )的参数化机制,你会发现,Java 泛型并不能满足所有的预期。由于泛型出来前,java已经有了很多项目了,为了兼容老版本,采用了泛型擦除来"实现泛型",这会遇到很多意料之外的麻烦,但这并不是说 Java 泛型毫无用处,它大多数情况能够让代码更加优雅,后面有机会我们会继续深入聊聊泛型擦除带来的麻烦及其历史渊源。
  参考资料:
  《On Java8》
  《Effective Java》
  https://www.liaoxuefeng.com/wiki/1252599548343744/1265102638843296
  https://www.cnblogs.com/mahuan2/p/6073493.html
  本篇文章到这里就结束啦,如果我的文章对你有所帮助,还请帮忙一键三连: 点赞、关注、收藏 ,你的支持会激励我输出更高质量的文章,感谢!
  计算机内功、JAVA源码、职业成长、项目实战、面试相关等更多高质量文章,首发于公众号「小牛呼噜噜」,我们下期再见。
幻塔蔷薇之锋搭配推荐幻塔蔷薇之锋怎么搭配?在幻塔这款游戏中,很多玩家不清楚蔷薇之锋该怎么搭配,下面为大家带来了幻塔蔷薇之锋搭配推荐,一起来看看吧。蔷薇之锋搭配推荐1金色意志推荐选择金色乌丸四件套,可以大衣嫂怒斥经纪人深秋,再胡说撕烂你的嘴,暗指她在背后使坏引子陈亚男和朱小伟离婚,确实有些太突然了,网友都以为是炒作,谁知却变成了现实。陈亚男一直说,是网络舆论导致她和朱小伟分手,其中到底有怎样的阴谋呢?大衣哥走红之后,身边就出现了很多经古巴现状,带你去看看真实的古巴文悠悠言地图,欢迎您的阅读!说起古巴,其实大家并不陌生,它是地处大洋彼岸的一个社会主义国家,跟中国关系向来友好,而且还很亲近,那么古巴究竟是什么样子的呢?咱们一起来看看真实的古巴。鸽了14年,耗资100亿美元!詹姆斯韦伯望远镜终于发射,将带来哪些新发现?JWST工作状态图片(图片来源NASA)刚刚,詹姆斯韦伯空间望远镜(JamesWebbSpaceTelescope,JWST,根据国家天文科学数据中心,其标准译名为韦布空间望远镜)大娘子刘琳,被名导抛弃,下嫁相亲男高龄产子,被婆家宠成宝2004年的一天,刘琳在出租车上看着报纸上的娱乐新闻哭的梨花带雨,曾经对她宣称不婚主义的张黎,转身便娶了京圈大飒蜜刘蓓!刘琳这才知道,原来他根本不是不想结婚,而是不想跟自己结婚。凭一个比一个浮夸,回看2021年的雷人演技,你们还是找个厂上班吧2021年已经临近尾声,今年的影视剧大家看着可还行?虽然佳作不少,但烂剧是一点都没减少啊,随着烂剧的增多,演员们的演技仿佛也跟着掉线了。回顾今年演员们的烂演技,可以说是一个比一个惊在上海买不了房的5位明星Twins缺购房资格,杨超越缺钱在鲁豫有约中,Twins称交社保攒积分,只能买到上海二手房引言。明星买房近几年,越来越多的港台明星,纷纷来到了内地发展,在看到内地娱乐圈待遇后,不少明星都有了定居内地的打算,并且开山东男篮险胜江苏,赛后发布会上队长陶汉林给出解释CBA第二阶段第14轮展,江苏队开场不错,上半场一度领先,下半场山东男篮发力,最终凭借高诗岩和吉伦沃特的3分球,山东男篮9387力克江苏,江苏遭遇3连败。赛后,队长陶汉林表示,复赛老校长用半年时间,让外孙从班级倒数变成年级前三,经验分享来了老校长用半年时间,让外孙从学渣变成学霸,老师都惊讶孩子的变化朋友的孩子成绩不好,经常是班级倒数。给孩子买的书,孩子也根本不看,有时候还拿朋友新买的书折纸飞机玩。朋友很生气,为此还教日本乒乓球男子王牌张本智和,刚被大学录取了张本智和成大学生了,早稻田大学人类科学部,18岁日刊体育报道乒乓球选手张本智和明年(2022年)春天进入早稻田大学人类科学部,不属于学校乒乓球部而是作为职业选手活动。据日媒二十四号抗衰老迎来新希望,专家提新建议,皱纹或许能少出现有很多人都在说衰老是自然界的规律,没有人能够打破它,你坚果的很多人,保养得很年轻,那是因为有足够的金钱和时间,像我们普通人用着二三百的护肤品,没日没夜的工作,为生活中的一些琐事而烦
华为Mate50将首发骁龙8984GiPhoneSE3或12月开始生产iPhoneSE3或12月开始生产10月9日,星期六,欢迎收看今天的科技V报,我是龙二Pro,对于苹果来说,一直有一个非常奇怪的产品线,那就是iPhoneSE系列,第一代iPhon小米亮剑,2K屏5000mAhIP68防水,高端旗舰机从4999跌至3659元在高端市场中,原本最受欢迎国产旗舰是华为Mate系列和P系列产品,不过由于如今的华为不能量产5G手机后,不仅高端市场份额下跌,其他价位上机型销量也是大幅度下跌。在目前的国产高端旗舰SONY在1976年推出的便携式音响,带有收音机电视盒式磁带录音机军绿色便携式音响这是1976年推出的军用便携式音响,有着军绿的颜色带有收音机电视盒式磁带录音机等功能有一个迷你CRT电视屏幕(观看航海者!太空1999GForce和奥特曼)精密模拟带孩子出游带上3镜1球,孩子玩得开心,专注力又好儿子小时候我就喜欢带他户外探险,因为室内游乐场往往空气流通不佳,玩来玩去都是那几样,激发不了孩子的创造性和主动探索的能力,而户外游会让孩子心胸更加宽广,性格也更加坚韧。在自然世界中官宣!火箭篮网达成交易,太阳迎来坏消息,本西离队有新进展北京时间10月7日,NBA季前赛已经开打。刚刚过去的24小时,联盟又传来多个重磅消息火箭篮网达成交易,双方各取所需太阳队迎来坏消息,主力中锋艾顿续约受阻湖人功勋大加索尔宣布退役,众OPPOReno7预计12月闪亮登场,Reno6上演降价狂潮,不只超冰点OPPOReno7预计12月份闪亮登场,OPPOReno7预计搭载联发科天玑1200处理器,OPPOReno7预计标配8G128G存储组合,OPPOReno7跑分在72万左右,OP绿联小海豚TWS耳机小巧舒适,百元性价比好耳机相比有线耳机,无线耳机没有了线材的束缚,携带更加方便,经过多年的技术积累,无线耳机在音质上有了长足进步,目前市面上的无线耳机琳琅满目,在激烈的竞争下价格越来越亲民,今天三爷为大家分拆解使用半年的飞利浦无线蓝牙耳机(PHILIPSSHB4205白)购买这款耳机的时候,就是看中了它三点,1,品牌,一直很喜欢philips这个品牌,它所传递给我的是品质优,耐用。(因为我的剃须刀就是飞利浦的,使用了8年还能跟初次使用一样美好)2,佩戴更舒适,音质更醇美,小巧的泰捷JEETAir2真无线蓝牙耳机体验对于很多人来说,蓝牙耳机已经是随身的常用电子产品,无论是生活工作,还是娱乐运动,蓝牙耳机的使用率都非常高,甚至有不少朋友拥有2个以上的蓝牙耳机,而之所以拥有多个蓝牙耳机,很大的原因魅蓝Blus真无线主动降噪蓝牙耳机图赏那个能打的魅蓝又回来了在魅族重启魅蓝子品牌后,首先推出的居然是一款价格不到200的带主动降噪功能的TWS蓝牙耳机!配置方面十分亮眼12mm大动圈单元30dB主动降噪双mic通话降噪30h超长续航,以及支什么样的人更适合使用骨传导耳机?以西圣X1为例,一文读懂前阵子给大家晒了一下我用的骨传导耳机,有位老哥提问,说自己工作很忙,周末更喜欢窝在电脑前,看剧还想留只耳朵给老婆,有没有必要选择骨传导耳机?答案是,可选,但非必要。比如说,选择支持