- Sản phẩm & Dịch vụSản phẩm & Dịch vụ
- Giải phápGiải pháp
- Bảng giáBảng giá
- Công tyCông ty
- Tài liệuTài liệu
vi
vi
Chuyên mục với các bài viết hướng dẫn, nghiên cứu và phân tích chi tiết về kỹ thuật, các xu hướng công nghệ mới nhất dành cho lập trình viên.
Technical Writer
I have over 5 years of experience writing technical documentation for tech products, making them accessible and user-friendly. My focus is always on providing clear and precise information. @#@ Tôi đã có hơn 5 năm kinh nghiệm viết tài liệu kỹ thuật cho các sản phẩm công nghệ, giúp người dùng dễ dàng tiếp cận và sử dụng. Tôi luôn tập trung vào việc cung cấp thông tin chính xác và dễ hiểu.
Thảo luận (0)
Đăng nhập để thảo luận

Trong bài viết này, chúng ta sẽ cùng thảo luận thêm về một số câu hỏi phỏng vấn Scala hữu ích dành cho những Scala Developers đã có kinh nghiệm.
Lưu ý: Vì danh sách câu hỏi đã khá dài, tôi sẽ viết thêm một bài khác để trình bày những câu hỏi và câu trả lời còn lại.
Trong phần này, chúng ta sẽ liệt kê tất cả các Scala Intermediate Interview Questions, và ở phần tiếp theo sẽ thảo luận chi tiết từng câu.
Trong phần này, chúng ta sẽ lần lượt lấy từng câu hỏi trong danh sách trên để thảo luận chi tiết, kèm theo ví dụ (nếu cần). Nếu bạn muốn hiểu rõ hơn với ví dụ cụ thể, hãy tham khảo các bài viết trước trong mục Scala Tutorials.
Trong Scala có hai loại constructor:
Primary Constructor
Trong Scala, Primary Constructor được định nghĩa ngay trong phần khai báo class. Mỗi class bắt buộc phải có một Primary Constructor: có thể là constructor có tham số hoặc không có tham số. Ví dụ:
class Person
Class Person ở trên có một Primary Constructor không tham số (Zero-parameter / No-Parameter / Parameterless) để tạo ra các instance của class này.
class Person (firstName: String, lastName: String)
Class Person ở trên có một Primary Constructor với hai tham số để tạo ra các instance của class này. Auxiliary Constructor Auxiliary Constructor còn được gọi là Secondary Constructor. Chúng ta có thể khai báo một Secondary Constructor bằng cách sử dụng các từ khóa “def” và “this” như trong ví dụ dưới đây:
class Person (firstName: String, middleName:String, lastName: String){
def this(firstName: String, lastName: String){
this(firstName, "", lastName)
}
}
Trong Scala, mục đích chính của Auxiliary Constructors là để overload constructors. Tương tự như trong Java, chúng ta có thể cung cấp nhiều loại constructors khác nhau để người dùng chọn tuỳ theo nhu cầu.
Quy tắc của Auxiliary Constructor:
Case class là một class được định nghĩa với từ khóa “case class”. Case object là một object được định nghĩa với từ khóa “case object”. Nhờ có từ khóa “case”, chúng ta sẽ nhận được một số lợi ích để tránh phải viết boilerplate code. Chúng ta có thể tạo object của case class mà không cần dùng từ khóa “new”. Mặc định, Scala compiler sẽ thêm tiền tố “val” cho tất cả constructor parameters. Vì vậy, ngay cả khi không khai báo val hoặc var, các tham số constructor của case class vẫn trở thành class members, điều này không thể thực hiện được với normal classes. Advantages of case class:
• Normal object được tạo bằng từ khóa object. Mặc định, nó là một singleton object.
object MyNormalObject
• Case Object được tạo bằng từ khóa “case object”. Mặc định, nó cũng là một singleton object.
case object MyCaseObject
• Mặc định, Case Object có các method toString và hashCode, còn normal object thì không.
• Mặc định, Case Object là Serializable, còn normal object thì không.
Dưới đây là những ưu điểm chính của Case Class so với Normal Class:
Cả hai phương thức isInstanceOf và asInstanceOf đều được định nghĩa trong class Any. Vì vậy không cần import để sử dụng trong bất kỳ class hay object nào.
Phương thức “isInstanceOf” được dùng để kiểm tra xem object có thuộc kiểu dữ liệu được chỉ định hay không. Nếu có thì trả về true, ngược lại trả về false.
scala> val str = "Hello"scala>str.isInstanceOf[String] res0: Boolean = false
Phương thức “asInstanceOf” được dùng để ép kiểu object sang kiểu dữ liệu được chỉ định. Nếu object và kiểu dữ liệu chỉ định là cùng kiểu, thì nó sẽ ép sang kiểu đó. Ngược lại, nó sẽ ném ra java.lang.ClassCastException.
scala> val str = "Hello".asInstanceOf[String] str: String = Hello
Trong Java, từ khóa “instanceof” tương tự với phương thức “isInstanceOf” trong Scala. Trong Java, việc ép kiểu thủ công giống với phương thức “asInstanceOf” trong Scala.
AccountService service = (AccountService)
context.getBean("accountService");
Vâng, mặc định Case Object là Serializable, còn Normal Object thì không. Chúng ta có thể chứng minh điều này bằng cách sử dụng phương thức isInstanceOf như được minh họa dưới đây:
scala> object MyNormalObject defined object MyNormalObjectscala> MyNormalObject.isInstanceOf[Serializable] res0: Boolean = false
scala> case object MyCaseObject defined object MyCaseObject
scala> MyCaseObject.isInstanceOf[Serializable] res1: Boolean = true
Lưu ý: Nếu bạn chưa chắc chắn về khái niệm Invariant và Covariant, vui lòng đọc bài viết tiếp theo của tôi về các câu hỏi phỏng vấn Scala.
Như đã thảo luận trong Basic Scala Interview Questions, “val” có nghĩa là giá trị hoặc hằng số, được dùng để định nghĩa các biến Immutable.
Có hai loại evaluation trong chương trình:
Eager Evaluation là việc chương trình được đánh giá ngay tại thời điểm biên dịch hoặc triển khai, bất kể người dùng có thực sự sử dụng hay không, trong khi Lazy Evaluation là việc chương trình chỉ được đánh giá tại thời điểm chạy khi có nhu cầu, nghĩa là chỉ khi người dùng truy cập thì nó mới thực sự được thực thi. Do đó, sự khác biệt giữa “val” và “lazy val” nằm ở cách đánh giá: “val” được đánh giá ngay lập tức, còn “lazy val” thì chỉ được đánh giá trì hoãn khi cần thiết.
Trong Scala, ta không cần trực tiếp gọi phương thức equals() để so sánh hai instance hay object. Khi ta so sánh hai instance bằng toán tử ==, Scala sẽ tự động gọi phương thức equals() của object đó. Trong Java, toán tử == được dùng để kiểm tra Reference Equality, tức là kiểm tra xem hai reference có trỏ tới cùng một object trong bộ nhớ hay không. Ngược lại, trong Scala, toán tử == được dùng để kiểm tra Instance Equality, tức là kiểm tra xem hai instance có bằng nhau về giá trị hay không.
Trong Java, Inner class gắn liền với Outer class, tức là nó được xem như một thành viên của Outer class. Ngược lại, trong Scala, mối quan hệ này được xử lý khác: Inner class trong Scala gắn liền với Outer class object, chứ không chỉ với Outer class nói chung.
Diamond Problem là một vấn đề trong Multiple Inheritance. Một số người còn gọi nó là Deadly Diamond Problem. Trong Scala, vấn đề này xảy ra khi một Class kế thừa từ nhiều Traits khác nhau nhưng các Traits đó lại có cùng một định nghĩa phương thức, như trong ví dụ dưới đây.

Không giống như Java 8, Scala giải quyết diamond problem một cách tự động bằng cách tuân theo một số quy tắc đã được định nghĩa trong ngôn ngữ. Những quy tắc này được gọi là “Class Linearization”. Ví dụ: –
trait A{
def display(){ println("From A.display") }
}
trait B extends A{
override def display() { println("From B.display") }
}
trait C extends A{
override def display() { println("From C.display") }
}
class D extends B with C{ }
object ScalaDiamonProblemTest extends App {
val d = new D
d display
}
Kết quả ở đây là “From C.display” từ trait C. Scala Compiler sẽ đọc phần “extends B with C” từ phải sang trái và lấy định nghĩa phương thức “display” từ trait ngoài cùng bên trái, tức là C.
Như chúng ta biết, Scala không hề có keyword “static”. Đây là một quyết định thiết kế có chủ đích từ Scala Team. Nguyên nhân chính là để biến Scala thành một Pure Object-Oriented Language. Keyword “static” có nghĩa là ta có thể truy cập vào các thành viên của class mà không cần tạo object hay không cần thông qua object. Điều này hoàn toàn đi ngược lại với nguyên tắc của OOP. Nếu một ngôn ngữ hỗ trợ “static”, thì ngôn ngữ đó không thể được coi là thuần hướng đối tượng. Ví dụ: Java có hỗ trợ “static” keyword, do đó Java không phải là một Pure Object-Oriented Language. Ngược lại, Scala là một Pure Object-Oriented Language.
Trong Scala, keyword object được dùng cho các mục đích sau:
object MySingletonObject
Ở đây, MySingletonObject tự động trở thành một singleton object. Keyword object được dùng để định nghĩa Scala Applications, tức là các chương trình Scala có thể thực thi.
object MyScalaExecutableProgram{
def main(args: Array[String]){
println("Hello World")
}
}
Khi ta định nghĩa main method trong một object như ví dụ trên (tương tự như main() method trong Java), chương trình đó sẽ tự động trở thành một executable Scala program. Keyword object cũng được dùng để định nghĩa các static members như static variables và static methods, nhưng không cần dùng đến keyword “static”.
object MyScalaStaticMembers{
val PI: Double = 3.1414
def add(a: Int, b: Int) = a + b
}
Khi ta định nghĩa biến PI và các phương thức add, chúng sẽ trở thành static members. Điều này có nghĩa là ta có thể gọi chúng mà không cần tạo một object riêng biệt, ví dụ: MyScalaStaticMembers.add(10, 20).
Ngoài ra, keyword object còn được dùng để định nghĩa Factory methods.
Trong Scala, ta sử dụng keyword object để định nghĩa Factory methods. Mục đích chính của các Factory methods này là tránh phải sử dụng keyword new khi tạo object. Nhờ đó, ta có thể tạo object mà không cần gọi trực tiếp new constructor.
Để định nghĩa Factory methods, ta có thể dùng apply method. Nếu một class có Primary Constructor và nhiều Auxiliary Constructors, ta sẽ định nghĩa nhiều apply methods tương ứng.
class Person(val firstName: String, val middleName: String, val lastName: String){
def this(firstName: String, lastName: String){
this(firstName,"",lastName)
}
}
object Person{
def apply(val firstName: String, val middleName: String, val lastName: String)
= new Person(firstName,middleName,lastName)
def apply(val firstName: String, val lastName: String)
= new Person(firstName, lastName)
}
Bây giờ, chúng ta có thể tạo các object Person không cần dùng keyword new, hoặc dùng new nếu muốn.
val p1 = new Person("Scala","Java")
or
val p1 = Person("Scala","Java")
Trong Scala, apply và unapply methods đóng vai trò rất quan trọng. Chúng cũng rất hữu ích trong Play Framework để mapping và unmapping dữ liệu giữa Form data và Model data.
Nói đơn giản:
Apply method trong Scala: Nó được dùng để tạo một object bằng cách sử dụng các thành phần của object đó. Ví dụ, nếu muốn tạo một object Person, ta sẽ dùng hai thành phần firstName và lastName để lắp ráp thành object Person như sau.
class Person(val firstName: String, val lastName: String)object Person{ def apply(firstName: String, lastName: String) = new Person(firstName, lastName) }
Unapply method trong Scala: Nó được dùng để phân rã một object thành các thành phần của nó. Đây là quá trình ngược lại với apply method. Ví dụ, nếu ta có một object Person, thì có thể phân rã object này thành hai thành phần firstName và lastName như minh họa dưới đây.
class Person(val firstName: String, val lastName: String)object Person{ def apply(firstName: String, lastName: String) = new Person(firstName, lastName)
def unapply(p: Person): (String,String) = (p.firstName, p.lastName)}
Trong Scala, khi chúng ta tạo một instance của Class mà không dùng từ khóa “new”, về cơ bản hệ thống sẽ gọi phương thức apply phù hợp có trong Companion object của class đó. Ở đây, “phương thức apply phù hợp” nghĩa là phương thức có tham số khớp với những gì chúng ta truyền vào.
Khi nào nên dùng cách này:
Cách làm là: chúng ta chỉ cần triển khai phương thức apply với cùng tập tham số như constructor private, từ đó cho phép người dùng class tạo object mà không cần dùng từ khóa new.
Trong Scala, việc khai báo một Primary Constructor private rất đơn giản. Chỉ cần định nghĩa Primary Constructor như bình thường và thêm từ khóa private ngay sau tên class và trước danh sách tham số như ví dụ dưới đây:
class Person private (name: String)
object Person{
def apply(name: String) = new Person(name)
}
Vì đây là private constructor, nên chúng ta không thể gọi trực tiếp từ bên ngoài. Thay vào đó, cần cung cấp một factory method (tức là phương thức apply như đã trình bày ở trên) và sử dụng constructor đó một cách gián tiếp.
Thông thường, các private member chỉ có thể truy cập bên trong chính class đó. Tuy nhiên, Scala cung cấp một tính năng đặc biệt cho Companion class và Companion object: trong Scala, một Companion object có thể truy cập các private member của Companion class, và ngược lại, Companion class cũng có thể truy cập các private member của Companion object.
Trong Scala, chúng ta dùng từ khóa class để định nghĩa các instance members và từ khóa object để định nghĩa các static members. Scala không có từ khóa static, nhưng vẫn có thể định nghĩa chúng bằng cách sử dụng từ khóa object. Quyết định thiết kế chính ở đây là để tạo ra sự phân tách rõ ràng giữa instance members và static members, giảm sự phụ thuộc lẫn nhau. Một lý do quan trọng khác là tránh dùng từ khóa static để Scala trở thành một ngôn ngữ lập trình hướng đối tượng thuần túy.
Khác với Java, trong Scala, từ khóa “object” có hai nghĩa. Đừng nhầm lẫn. Trong Java, object chỉ có một nghĩa duy nhất là “một instance của class”.
• Tương tự như Java, nghĩa thứ nhất của object trong Scala cũng là “một instance của class”.
val p1 = new Person("Scala","Java")
or
val p1 = Person("Scala","Java")
• Nghĩa thứ hai là object là một từ khóa trong Scala. Nó được dùng để định nghĩa các chương trình có thể thực thi trong Scala, Companion Objects, Singleton Objects, v.v.
Nói một cách đơn giản, nếu một class và một object trong Scala có cùng tên và được định nghĩa trong cùng một file nguồn, thì class đó được gọi là “Companion Class” và object đó được gọi là “Companion Object”.
Khi chúng ta tạo một Class bằng từ khóa “class” và một Object bằng từ khóa “object” với cùng tên và trong cùng file nguồn, thì class đó được gọi là “Companion Class” và object đó được gọi là “Companion Object”.
Ví dụ: Employee.scala
class Employee{ }
object Employee{ }
Trong Scala, mục đích chính của Companion Object là để định nghĩa các phương thức apply và tránh việc sử dụng từ khóa new khi tạo instance của Companion class đó.
Như chúng ta biết từ Java, ta dùng interface để định nghĩa hợp đồng. Tuy nhiên, trong Scala không có khái niệm interface. Thậm chí, Scala cũng không có từ khóa interface. Thay vào đó, Scala có một khái niệm mạnh mẽ và linh hoạt hơn, đó là trait, để thực hiện mục đích này.
Range là một Lazy Collection trong Scala. Range là một class có sẵn trong package “scala”, ví dụ “scala.Range”. Nó được dùng để biểu diễn một dãy các giá trị nguyên theo thứ tự.
Ví dụ:
scala> 1 to 10 res0: scala.collection.immutable.Range.Inclusive = Range(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)scala> 1 until 10 res1: scala.collection.immutable.Range = Range(1, 2, 3, 4, 5, 6, 7, 8, 9)
Trong Scala, kiểu Nothing không có giá trị nào cả. Nó là subtype của tất cả các Value classes và Reference classes.
Trong Scala, Unit tương tự như từ khóa void trong Java. Nó được dùng để biểu diễn “không có giá trị”. Kiểu Unit chỉ có một giá trị duy nhất, đó là ().
Pure function là một hàm không có bất kỳ side-effect quan sát được nào. Nghĩa là, hàm luôn trả về kết quả giống nhau bất kể chúng ta gọi nó bao nhiêu lần với cùng đầu vào. Một pure function luôn cho cùng một output với cùng một input.
Ví dụ:
scala> 10 + 20 res0: Int = 30 scala> scala> 10 + 20 res0: Int = 30
Ở đây, phép “+” là một pure function có sẵn trong class Int. Nó luôn cho kết quả giống nhau là 30 khi cộng 10 và 20, bất kể chúng ta gọi nó bao nhiêu lần.
Cả hai đều được dùng để thực hiện tính toán, tuy nhiên trong lập trình hàm có một khác biệt chính: Function là một đơn vị tính toán không có side-effect, trong khi Procedure là một đơn vị tính toán có side-effect.
Auxiliary constructors trong Scala gần giống với constructors trong Java nhưng có một vài điểm khác:
Chúng ta có thể sử dụng từ khóa “yield” trong cấu trúc for-comprehension của Scala. Cấu trúc “for/yield” được dùng để lặp qua một collection các phần tử và tạo ra một collection mới cùng kiểu với collection gốc. Nó không thay đổi collection ban đầu, mà sinh ra một collection mới cùng loại với collection gốc.
Ví dụ: nếu dùng “for/yield” để lặp qua một List, kết quả sẽ là một List mới.
scala> val list = List(1,2,3,4,5) list: List[Int] = List(1, 2, 3, 4, 5)scala> for(l <- list) yield l*2 res0: List[Int] = List(2, 4, 6, 8, 10)
Trong Scala, cấu trúc for-comprehension có một mệnh đề if được dùng để viết điều kiện nhằm lọc một số phần tử và tạo ra collection mới. Mệnh đề if này còn được gọi là “Guard”. Nếu guard đó đúng thì phần tử sẽ được thêm vào collection mới, ngược lại thì phần tử đó sẽ không được thêm vào collection mới.
Ví dụ: For-comprehension Guard để tạo ra collection chỉ chứa các số chẵn.
scala> val list = List(1,2,3,4,5,6,7,8,9,10) list: List[Int] = List(1, 2, 3, 4, 5 , 6 , 7 , 8 , 9 , 10)scala> for(l <- list if l % 2 =0 ) yield l res0: List[Int] = List(2, 4, 6, 8, 10)
Nếu dùng Interface với Default methods trong Java 8, chúng ta sẽ gặp Inheritance Diamond Problem. Lập trình viên phải tự xử lý thủ công vì Java 8 không cung cấp cách giải quyết mặc định hay tự động cho vấn đề này. Trong Scala, khi dùng Traits cũng sẽ gặp vấn đề tương tự, nhưng Scala giải quyết Inheritance Diamond Problem một cách tự động nhờ cơ chế Class Linearization.
Trong Scala, Pattern Matching tuân theo Visitor Design Pattern. Tương tự, trong Java, toán tử “instanceof” cũng tuân theo Visitor Design Pattern. Đó là toàn bộ phần “Scala Intermediate Interview Questions and Answers”. Trong các bài viết tiếp theo, chúng ta sẽ thảo luận về Advanced Scala Interview Questions and Answers. Vui lòng để lại bình luận nếu bạn thích bài viết hoặc có bất kỳ thắc mắc/đề xuất nào.