23篇大数据系列(二)scala基础知识全集(史上最全,建议收藏)
作者簡介:
藍橋簽約作者、大數據&Python領域優質創作者。管理多個大數據技術群,幫助大學生就業和初級程序員解決工作難題。
我的使命與愿景:持續穩定輸出,賦能中國技術社區蓬勃發展!
大數據系列文章,從技術能力、業務基礎、分析思維三大板塊來呈現,你將收獲:
??提升自信心,自如應對面試,順利拿到實習崗位或offer;
??掌握大數據的基礎知識,與其他同事溝通無障礙;
??具備一定的項目實戰能力,對于大數據工作直接上手;
評論、點贊、收藏是對我最大的支持!!!
大數據工程師系列專欄:?面試真題、開發經驗、調優策略
大數據工程師知識體系:
大數據時代已經到來
最近幾十年,高速發展的互聯網,滲透進了我們生活的方方面面,整個人類社會都已經被互聯網連接為一體。身處互聯網之中,我們無時無刻不在產生大量數據,如瀏覽商品的記錄、成交訂單記錄、觀看視頻的數據、瀏覽過的網頁、搜索過的關鍵詞、點擊過的廣告、朋友圈的自拍和狀態等。這些數據,既是我們行為留下的痕跡,同時也是描述我們自身最佳的證據。
2014年3月,馬云曾經在北京的一次演講中說道:“人類正從IT時代走向DT時代”。7年過去了,正如馬云預想的那樣,大數據時代已經到來了。
大數據工程師的工作內容是什么?
而大數據時代,有一個關鍵性的崗位不得不提,那就是大數據工程師。想必大家也會好奇,大數據工程師,日常是做什么的呢??
| 1.數據采集 | 找出描述用戶或對業務發展有幫助的數據,并將定義相關的數據格式,交由業務開發部門負責收集對應的數據。 |
| 2.ETL工程? | 對收集到的數據,進行各種清洗、處理、轉化等操作,完成格式轉換,便于后續分析,保證數據質量,以便得出可以信賴的結果。 |
| 3.構建數倉 | 將數據有效治理起來,構建統一的數據倉庫,讓數據與數據間建立連接,碰撞出更大的價值。 |
| 4.數據建模 | 基于已有的數據,梳理數據間的復雜關系,建立恰當的數據模型,便于分析出有價值的結論。 |
| 5.統計分析 | 對數據進行各種維度的統計分析,建立指標體系,系統性地描述業務發展的當前狀態,尋找業務中的問題,發現新的優化點與增長點。 |
| 6.用戶畫像 | 基于用戶的各方面數據,建立對用戶的全方位理解,構建每個特定用戶的畫像,以便針對每個個體完成精細化運營。 |
大數據工程師必備技能
那么,問題來了,如果想成為一名大數據工程師,勝任上述工作內容,需要具備什么樣的條件?擁有什么樣的知識呢?
| 分類 | 子分類 | 技能 | 描述 |
| 技 術 能 力 | 編程基礎 | Java基礎 | 大數據生態必備的java基礎 |
| Scala基礎 | Spark相關生態的必備技能 | ||
| SQL基礎 | 數據分析師的通用語言 | ||
| SQL進階 | 完成復雜分析的必備技能 | ||
| 大數據框架 | HDFS&YARN | 大數據生態的底層基石 | |
| Hive基礎 | 大數據分析的常用工具 | ||
| Hive進階 | 大數據分析師的高級裝備 | ||
| Spark基礎 | 排查問題必備的底層運行原理 | ||
| Spark SQL | 應對復雜任務的利刃 | ||
| 工具 | Hue&Zeppelin | 通用的探索分析工具 | |
| Azkaban | 作業管理調度平臺 | ||
| Tableau | 數據可視化平臺 | ||
| 業務基礎 | 數據收集 | 數據是如何收集到的? | |
| ETL工程 | 怎么清洗、處理和轉化數據? | ||
| 數據倉庫基礎 | 如何完成面向分析的數據建模? | ||
| 元數據中心 | 如何做好數據治理? | ||
| 分析思維 | 數據分析思維方法論 | 怎么去分析一個具體問題? | |
| 排查問題思維 | 如何高效排查數據問題? | ||
| 指標體系 | 怎么讓數據成體系化? | ||
Scala為什么會如此重要,作者覺得主要有以下三點原因:
1、因為spark?
大部分從事大數據的工程師是先了解Spark進而再去選擇學習Scala的,因為Spark是用Scala開發的。現在Spark是大數據領域的殺手級應用框架,只要搭建了大數據平臺,都會大量使用Spark來處理和分析數據,而要想學好Spark,Scala這一關必須是要過的。順便說一句,Kafka也是基于Scala開發的。
2、無縫對接大數據生態組件?
眾所周知,大數據生態的大部分組件都是java語言開發的。而Scala是一門基于JVM的語言,可以與java無縫混編,因此可以很好地融合到大數據生態圈。
3、適合大數據處理與機器學習?
Scala的語法簡潔而富有表達力,更容易掌握。Scala將面向對象與函數式編程相結合,功能強大且簡練,非常適合用于處理各種數據。因此,在大數據處理與機器學習中占有重要的地位。
針對大數據分析師必須掌握的scala基礎知識,本文的講解思路如下:
第1部分:scala特性。主要講解面向對象特性、函數式編程、靜態類型、擴展性和并發性。
第2部分:表達式。在scala中一切皆為表達式,理解表達式是理解其語法的前提。
第3部分:方法與函數。主要講兩者之間的區別和轉換。
第4部分:模式匹配。講解常用的幾種模式,并舉例說明。
第5部分:scala?trait。講解特質的基本特性和示例。
第6部分:集合操作。主要針對常用集合和集合函數的講解和介紹。
第7部分:讀取數據源。只針對scala如何通過Source類讀取數據源進行簡單介紹。
第8部分:隱式轉換、隱式參數。主要講解Java和scala之間的類型轉換,以及通過一個實例介紹一下隱式參數的概念。
第9部分:正則匹配。主要講解如何寫正則相關的代碼。
第10部分:異常處理。介紹scala和java的異常有何區別。
第11部分:類型層級。主要介紹scala的類型層級體系。
第12部分:基本數值類型轉換。講解scala與java基本數值類型轉換常遇到的問題。
scala基礎知識
一、Scala特性
面向對象特性?
Scala是一種純面向對象的語言,徹底貫徹萬物皆對象理念。對象的類型和行為是由類和特質來描述的。Scala引入特質(trait)來改進Java的對象模型,使得可以通過混入特質的方式,擴展類的功能。
函數式編程?
Scala也是一種函數式語言,函數也能當成值來傳遞。Scala提供了輕量級的語法用以定義匿名函數,支持高階函數,允許嵌套多層函數,并支持柯里化。Scala的case class及其內置的模式匹配相當于函數式編程語言中常用的代數類型。
靜態類型?
Scala擁有一個強大表達能力的類型系統,通過編譯時檢查,保證代碼的安全性和一致性。Scala具備類型推斷的特性,這使得開發者可以不去額外標明重復的類型信息,讓代碼看起來更加整潔易讀。
?擴展性?
Scala的設計秉承一項事實,即在實踐中,某個領域特定的應用程序開發往往需要特定于該領域的語言擴展。Scala提供了許多獨特的語言機制,可以以庫的形式輕易無縫添加新的語言結構。
二、表達式
在scala中,一切皆為表達式。scala非常推崇表達式語法,因為表達式語法,對函數式編程是非常友好的。對開發者而言,表達式語法,使得代碼非常簡潔易讀。
舉個例子,我們在定義方法時,會和聲明變量一樣,使用等號(=)連接,等號左側是函數名、參數列表和返回值類型(可以省略),而等號右邊便是一個由大括號({})包裹的多行表達式。
表達式,是一定會有返回值的。在java中使用void來聲明無返回值的方法,而在scala里,這種情況也會有返回值,會返回一個Unit,這是一個特定的值,表示忽略方法的返回值。
三、方法與函數
初學scala時,往往會覺得方法和函數的概念有些模糊,在使用中可能會搞不清楚到底該使用方法還是函數。那怎么區分呢?關鍵是看這個函數是否在類中定義,在類中定義就是方法,所以Scala 方法是類的一部分。Scala 中的函數則是一個完整的對象,可以賦給一個變量。不過,在scala中,方法和函數是可以相互轉化的。下面我們重點說下,如何把方法轉為函數。
方法轉函數
上文中提到任何方法都是在聲明一個表達式,所以將方法轉為函數也就非常簡單了,相當于是把方法指向的表達式,又重新賦給了一個函數變量,這就是顯式轉化。還有另外一種寫法,是通過偏應用函數的方式,將方法轉化為一個新的函數,稱作隱式轉化。
1)隱式轉化
val f2 = f1 _2)顯式轉化
val f2: (Int) => Int = f1四、模式匹配
模式匹配是檢查某個值是否匹配某一個模式的機制。它是Java中的switch語句的升級版,同樣可以用于替代一系列的 if/else 語句,以下介紹幾種常用的模式匹配:常量模式、變量模式、通配符模式。
常量模式
常量模式匹配,就是在模式匹配中匹配常量。
object ConstantPattern{ def main(args:Array[String]) :Unit = { //模式匹配結果作為函數返回值 def patternShow(x : Any) = x match { //常量模式 case 5 => "五" case true => "真" case "test" => "字符串" case null => "null值" case Nil => "空列表" //變量模式 case x => "變量" //通配符模式 case _ => "通配符" } }} 變量模式和通配符模式,都可以匹配任意值,他們之間的區別是,變量模式匹配成功后,該變量中會存儲匹配成功的值,在后續的代碼中還可以引用,而通配符模式匹配成功后,不能再引用匹配到的值。另外要注意的是,由于模式匹配是按順序匹配的,因此變量模式和通配符模式要寫在表達式的最后面。類型匹配模式
可以匹配輸入變量的類型。
object TypePattern{ def main(args:Array[String]) :Unit = { //類型匹配模式 def typePattern(t : Any) = t match { case t : String => "String" case t : Int => "Intger" case t : Double => "Double" case _ => "Other Type" } }}case class模式
構造器模式指的是,直接在case語句后面接類構造器,匹配的內容放置在構造器參數中。
object CaseClassPattern{ def main(args:Array[String]) :Unit = { //定義一個Person實例 val p = new Person("nyz",27) //case class 模式 def constructorPattern(p : Person) = p match { case Person(name,age) => "name =" + name + ",age =" + age case _ => "Other" } }}模式守衛
為了讓匹配更加具體,可以使用模式守衛,也就是在模式后面加上if判斷語句。
object ConstantPattern{ def main(args:Array[String]) :Unit = { //模式匹配結果作為函數返回值 def patternShow(x : Any) = x match { //模式守衛 case x if(x == 5) => "守衛" //通配符模式 case _ => "通配符" } }}Option匹配
在Scala中Option類型樣例類用來表示可能存在或也可能不存在的值(Option的子類有Some和None)。Some包裝了某個值,None表示沒有值。
class OptionDemo { val map = Map (("a",18),("b",81))??//get方法返回的類型就是Option[Int] map.get("b") match { case some(x) => println(x) case None => println("不存在") }}五、Scala Trait(特質)
Scala Trait(特質) 相當于 Java 的接口,但實際上它比接口的功能強大。與接口不同的是,它還可以定義屬性和方法的實現。
一般情況下Scala的類只能夠繼承單一父類,但可以使用with關鍵字混入多個 Trait(特質) 。不過,如果一個scala類沒有父類,那么它混入的第一個特質需要使用extends關鍵字,之后混入的特質使用with關鍵字。
Trait(特質) 定義的方式與類相似,但它使用的關鍵字是 trait,如下所示:
trait Equal { def isEqual(x: Any): Boolean def isNotEqual(x: Any): Boolean = !isEqual(x)}以上特質(Equal)由兩個方法組成:isEqual 和 isNotEqual。isEqual 方法沒有定義方法的實現,isNotEqual定義了方法的實現。子類繼承特質可以實現未被實現的方法。
以下演示了特質的完整實例:
trait Equal { def isEqual(x: Any): Boolean def isNotEqual(x: Any): Boolean = !isEqual(x)} class Point(xc: Int, yc: Int) extends Equal { val x: Int = xc val y: Int = yc def isEqual(obj: Any) = obj.isInstanceOf[Point] && obj.asInstanceOf[Point].x == x} object Test { def main(args: Array[String]) { val p1 = new Point(2, 3) val p2 = new Point(2, 4) val p3 = new Point(3, 3) println(p1.isNotEqual(p2)) println(p1.isNotEqual(p3)) println(p1.isNotEqual(2)) }}執行以上代碼,輸出結果為:
$ scalac Test.scala $ scala -cp . Testfalsetruetrue六、集合操作
常用集合
通過下面的代碼,可以了解常用集合的創建方式
// 定義整型 List,其元素以線性方式存儲,可以存放重復對象。val x = List(1,2,3,4) // 定義 Set,其對象不按特定的方式排序,并且沒有重復對象。val x = Set(1,3,5,7) // 定義 Map,把鍵對象和值對象映射的集合,它的每一個元素都包含一對鍵對象和值對象。val x = Map("one" -> 1, "two" -> 2, "three" -> 3) // 創建兩個不同類型元素的元組,元組是不同類型的值的集合val x = (10, "Bigdata") // 定義 Option,表示有可能包含值的容器,也可能不包含值。val x:Option[Int] = Some(5)集合函數
工作中操作 Scala 集合時,一般會進行兩類操作:轉換操作(transformation )和行動操作(action)。第一種操作類型將集合轉換為另一個集合,第二種操作類型返回某些類型的值。
1)最大值和最小值
先從行動函數開始。在序列中查找最大或最小值是一個極常見的需求。
先看一下簡單的例子。
val numbers = Seq(11, 2, 5, 1, 6, 3, 9) numbers.max //11 numbers.min //1對于這種簡單數據集合,Scala的函數式特性顯露無疑,如此簡單的取到了最大值和最小值。再來看一個數據集合復雜的例子。
case class Book(title: String, pages: Int) val books = Seq( Book("Future of Scala developers", 85), Book("Parallel algorithms", 240), Book("Object Oriented Programming", 130), Book("Mobile Development", 495)) //下面代碼返回Book(Mobile Development,495)books.maxBy(book => book.pages) //下面代碼返回Book(Future of Scala developers,85)books.minBy(book?=>?book.pages)minBy & maxBy方法解決了復雜數據的問題。
2)篩選-Filter
對集合進行過濾,返回滿足條件的元素的新集合,比如過濾一組數據中的偶數。
val numbers = Seq(1,2,3,4,5,6,7,8,9,10) numbers.filter(n => n % 2 == 0)//上面返回Seq(2,4,6,8,10)獲取頁數大于300頁的書。
val books = Seq( Book("Future of Scala developers", 85), Book("Parallel algorithms", 240), Book("Object Oriented Programming", 130), Book("Mobile Development", 495)) books.filter(book => book.pages >= 300)//上面返回Seq(Book("Mobile?Development",?495))還有一個與 filter類似的方法是 filterNot,也就是篩選出不滿足條件的對象。
3)Flatten
它的作用是將多個集合展開,組成一個新的集合,舉例說明。
val abcd = Seq('a', 'b', 'c', 'd')val efgj = Seq('e', 'f', 'g', 'h')val ijkl = Seq('i', 'j', 'k', 'l')val mnop = Seq('m', 'n', 'o', 'p')val qrst = Seq('q', 'r', 's', 't')val uvwx = Seq('u', 'v', 'w', 'x')val yz = Seq('y', 'z') val alphabet = Seq(abcd, efgj, ijkl, mnop, qrst, uvwx, yz) alphabet.flatten執行后返回下面的集合:
List('a',?'b',?'c',?'d',?'e',?'f',?'g',?'h',?'i',?'j',?'k',?'l',?'m',?'n',?'o',?'p',?'q',?'r',?'s',?'t',?'u',?'v',?'w',?'x',?'y',?'z')4)集合運算函數
集合運算即差集、交集和并集操作。
val num1 = Seq(1, 2, 3, 4, 5, 6)val num2 = Seq(4, 5, 6, 7, 8, 9) //返回List(1, 2, 3)num1.diff(num2) //返回List(4, 5, 6)num1.intersect(num2) //返回List(1, 2, 3, 4, 5, 6, 4, 5, 6, 7, 8, 9)num1.union(num2) //合并后再去重,返回List(1, 2, 3, 4, 5, 6, 7, 8, 9)num1.union(num2).distinct5)map函數
map 函數的邏輯是遍歷集合并對每個元素調用傳入的函數進行處理。
val numbers = Seq(1,2,3,4,5,6) //返回List(2, 4, 6, 8, 10, 12)numbers.map(n => n * 2) val chars = Seq('a', 'b', 'c', 'd') //返回List(A, B, C, D)chars.map(ch?=>?ch.toUpper)6)flatMap
它將map & flatten組合起來,請看下面的操作。
val abcd = Seq('a', 'b', 'c', 'd') //List(A, a, B, b, C, c, D, d)abcd.flatMap(ch?=>?List(ch.toUpper,?ch))從結果可以看出來是先做的map,然后做的flatten
7)forall?& exists
forall是對整個集合做判斷,當集合中的所有元素都滿足條件時,返回true。而exists則是只要有一個元素滿足條件就返回true。
val numbers = Seq(3, 7, 2, 9, 6, 5, 1, 4, 2) //返回turenumbers.forall(n => n < 10) //返回falsenumbers.forall(n => n > 5) //返回truenumbers.exists(n?=>?n?>?5)七、讀取數據源
讀取外部數據源是開發中很常見的需求,如在程序中讀取外部配置文件并解析,獲取相應的執行參數。這里只針對scala如何通過Source類讀取數據源進行簡單介紹。
import scala.io.Sourceobject ReadFile { //讀取ClasPath下的配置文件 val file = Source.fromInputStream(this.getClass.getClassLoader.getResourceAsStream("app.conf")) //一行一行讀取文件,getLines()表示讀取文件所有行 def readLine: Unit ={ for(line <- file.getLines()){ println(line) } } //讀取網絡上的內容 def readNetwork: Unit ={ val file = Source.fromURL("http://www.baidu.com") for(line <- file.getLines()){ println(line) } } //讀取給定的字符串-多用于調試 val source = Source.fromString("test") }八、隱式轉換
隱式轉換是Scala中一種非常有特色的功能,是其他編程語言所不具有的,可以實現將某種類型的對象轉換為另一種類型的對象。數據分析工作中,最常使用到的就是java和scala集合之間的互相轉換,轉換以后就可以調用另一種類型的方法。scala提供了scala.collection.JavaConversions類,只要引入此類中相應的隱式轉化方法,在程序中就可以用相應的類型來代替要求的類型。
如通過以下轉換,scala.collection.mutable.Buffer自動轉換成了java.util.List。
import scala.collection.JavaConversions.bufferAsJavaListscala.collection.mutable.Buffer?=>?java.util.List同樣,java.util.List也可以轉換成scala.collection.mutable.Buffer。
import scala.collection.JavaConversions.asScalaBufferjava.util.List?=>?scala.collection.mutable.Buffer所有可能的轉換匯總如下,雙向箭頭表示可互相轉換,單箭頭則表示只有左邊可轉換到右邊。
import scala.collection.JavaConversions._ scala.collection.Iterable <=> java.lang.Iterablescala.collection.Iterable <=> java.util.Collectionscala.collection.Iterator <=> java.util.{ Iterator, Enumeration }scala.collection.mutable.Buffer <=> java.util.Listscala.collection.mutable.Set <=> java.util.Setscala.collection.mutable.Map <=> java.util.{ Map, Dictionary }scala.collection.concurrent.Map <=> java.util.concurrent.ConcurrentMap scala.collection.Seq => java.util.Listscala.collection.mutable.Seq => java.util.Listscala.collection.Set => java.util.Setscala.collection.Map => java.util.Mapjava.util.Properties???=>?scala.collection.mutable.Map[String,?String]隱式參數
所謂隱式參數,指的是在函數或者方法中,定義使用implicit修飾的參數。當調用該函數或方法時,scala會嘗試在變量作用域中找到一個與指定類型相匹配的使用implicit修飾的對象,即隱式值,注入到函數參數中函數體使用。示例如下:
class SayHello{ def write(content:String) = println(content)}implicit val sayHello=new SayHello def saySomething(name:String)(implicit sayHello:SayHello){ sayHello.write("Hello," + name)} saySomething("Scala") //打印 Hello,Scala值得注意的是,隱式參數是根據類型匹配的,因此作用域中不能同時出現兩個相同類型的隱式變量,否則編譯時會拋出隱式變量模糊的異常。
九、正則匹配
正則的概念、作用和規則都在上一篇《大數據分析工程師入門--1.Java基礎》中已經完整的講述了,這里將通過示例來講解下在scala中正則相關代碼怎么寫:
定義
val?TEST_REGEX?=?"home\\*(classification|foundation|my_tv)\\*[0-9-]{0,2}([a-z_]*)".r使用
//path是用來匹配的字符串TEST_REGEX findFirstMatchIn path match { case Some(p) => { //獲取TEST_REGEX中的第一個括號里正則片段匹配到的內容 launcher_area_code = p.group(1) //獲取TEST_REGEX中的第二個括號里正則片段匹配到的內容 launcher_location_code = p.group(2) }}十、異常處理
學習過Java的同學對異常一定并不陌生,異常通常是程序執行過程中遇到問題時,用來打斷程序執行的重要方式。關于異常處理的注意事項,在上一講《大數據分析工程師入門--1.Java基礎》里已經講過了,這里就不再贅述了。我們重點來講下scala和java在異常這個特性的設計上的不同。
1. 捕獲異常的方式略有不同
java中是通過多個catch子句來捕獲不同類型的異常,而在scala中是通過一個catch子句,加上模式匹配的類型匹配方式來捕獲不同類型的異常。如下圖所示:
2.scala沒有checked異常
在java中,非運行時異常在編譯期是會被強制檢查的,要么寫try...catch...處理,要么使用throws關鍵字,將異常拋給調用者處理。而在scala中,更推崇通過使用函數式結構和強類型來減少對異常及其處理的依賴。因此scala不支持檢查型異常(checked exception)。
當使用scala調用java類庫時,scala會把java代碼中聲明的異常,轉換為非檢查型異常。
3.scala在throw異常時是有返回值的
在scala的設計中,所有表達式都是有返回值的。那么,自然throw表達式也不例外,throw表達式的返回值為Nothing。由于Nothing類型是所有類型的子類型,因此throw表達式可以出現在任意位置,而不會影響到類型的推斷。
十一、類型層級
在scala中,所有的值都是有類型的,包括數值型值和函數,比java更加徹底地貫徹了萬物皆對象的理念。因此,scala有一套自己的類型層級,如下圖所示:
(圖片來自于網絡)
如圖中所示,scala的頂級類是Any,下面包含兩個子類,AnyVal和AnyRef,其中AnyVal是所有值類型的父類,其中包含一個特殊的值Unit;而AnyRef是所有引用類型的父類,所有java類型和非值類型的scala類型都是它的子類。其中,有兩個比較特殊的底層子類型,一個是Null,它是所有引用類型的子類型,可以賦給任何引用類型變量;另一個是Nothing,它是所有類型的子類,因此既可以賦給引用類型變量,也可以賦給值類型變量。
十二、基本數值類型轉換
在scala中,通常會自動進行java和scala之間基本數值類型的轉換,并不需要單獨去處理。所以,在我們的感受中,通常java和scala的基本數據類型是可以無縫銜接的。但是,有一種情況是例外的,那就是當你引用第三方的java類庫,而在它的代碼中接收參數是Object類型,之后又對傳入對象的實際數值類型做判斷時,通常會失敗報錯。
原因很簡單,第三方java類庫,使用java語言編寫,它只認得java的類型。當接收參數為Object類型時,scala默認不會轉換成java的數值類型,這樣當判斷對象的具體數值類型時,會出現不認識scala對象類型的異常。
解決方案也很簡單,只需要在傳入第三方類庫方法前,手動包裝成java類型即可。以下是代碼示例,本例演示了DBUtils類庫傳入scala類型時的處理,只展示了部分代碼:
//由于java和scala中的類型短名稱重名,為避免歧義,進行了重命名import java.lang.{Long => JLong, Double => JDouble}//conn為數據庫連接,sql為要執行的SQL語句queryRunner.update(conn,?sql,?new?JLong(1L),?new?JDouble(2.2))總結
本文結合實際工作經驗,把scala中最常用到的一些知識點進行了梳理,要想成為一名初級大數據工程師,這些知識是必須要掌握的。
大數據俱樂部、數據與智能:??https://blog.csdn.net/weixin_39032019/article/details/117997723
整理不易,評論、點贊、收藏是對我最大的支持!!!
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的23篇大数据系列(二)scala基础知识全集(史上最全,建议收藏)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: opencv图像处理中的一些滤波器+利用
- 下一篇: Android之记住密码与自动登陆实现