Содержание


Путеводитель по Scala для Java-разработчиков

Пакеты и модификаторы доступа

Закрытый, защищенный и промежуточный уровни доступа в Scala

Comments

Серия контента:

Этот контент является частью # из серии # статей: Путеводитель по Scala для Java-разработчиков

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Путеводитель по Scala для Java-разработчиков

Следите за выходом новых статей этой серии.

Просматривая недавние комментарии читателей, я понял, что в предыдущих статьях серии не был рассмотрен один важный момент: организация пакетов и модификаторов доступа в Scala. В данной статье мы остановимся на этих вопросах перед тем, как переходить к более функциональным элементам языка, а именно к механизму применения (apply).

Пакеты

Ключевое слово package служит в Java™ для определения лексического пространства имен, в котором объявляются классы. Это позволяет разбивать код на пакеты, избегая при этом конфликтов имен. Помещение класса Foo в пакет com.tedneward.util фактически меняет полное имя класса на com.tedneward.util.Foo, именно так теперь к нему следует обращаться. Java-разработчики могут возразить, что это не обязательно, поскольку можно использовать директиву import, которая позволяет обращаться к классам по коротким именам. Это справедливо, однако всего лишь означает, что действия по обращению к классу по его полному имени ложатся на плечи компилятора, что отражается в байт-коде. Это легко подтверждается при помощи утилиты javap.

Пакеты в Java обладают рядом особенностей. Объявление каждого пакета должно находиться в самом начале файла .jar, в котором содержатся классы, принадлежащие данному пакету (это приводит к серьезным трудностям при попытке аннотирования пакетов). При этом объявление пакета относится ко всему файлу целиком. В частности, это приводит к тому, что в тех редких случаях, когда тесно связанные между собой классы оказываются в разных пакетах, их приходится разносить по разным файлам, вследствие чего связь между классами становится неочевидной.

В Scala принят несколько другой подход к организации пакетов, сочетающий в себе черты объявлений пакетов в Java и областей видимости в C#. Разработчики могут использовать традиционный механизм, принятый в Java, и помещать объявление package в начале файлов .scala. В этом случае объявление пакета, как и в Java, будет относиться ко всему файлу целиком. Существует альтернативный способ, при котором область действия пакета (оператора package) явно ограничивается фигурными скобками. Пример приведен в листинге 1.

Листинг 1. Простой пример использования пакетов
package com
{
  package tedneward
  {
    package scala
    {
      package demonstration
      {
        object App
        {
          def main(args : Array[String]) : Unit =
          {
            System.out.println("Howdy, from packaged code!")
            args.foreach((i) => System.out.println("Got " + i) )
          }
        }
      }
    }
  }
}

В данном примере объявляется один класс App (или, если быть более точным, класс с полным именем com.tedneward.scala.demonstration.App). Обратите внимание на то, что в Scala имена пакетов могут разделяться точками, поэтому фрагмент кода, приведенный в листинге 1, можно сократить, как показано в листинге 2.

Листинг 2. Еще один простой пример использования пакетов
package com.tedneward.scala.demonstration
{
  object App
  {
      def main(args : Array[String]) : Unit =
      {
        System.out.println("Howdy, from packaged code!")
        args.foreach((i) => System.out.println("Got " + i) )
      }
  }
}

Вы можете использовать любой из приведенных выше вариантов по своему усмотрению, поскольку они оба компилируются в абсолютно одинаковый байт-код. Компилятор scalac поместит скомпилированные файлы .class в директории, соответствующие именам пакетов, аналогично тому, как работает javac.

Импорт

Разумеется, обратной по отношению к package является директива import, служащая в Scala для обращения к классам из текущего пространства имен. Те из вас, кто читал предыдущие статьи, уже видели примеры использования import, но сейчас пришло время рассказать об особенностях данной директивы, которые могут оказаться удивительными для Java-разработчиков.

Во-первых, директива import может использоваться в любом месте в исходном коде, а не только в начале файла, что влияет на область ее применения. Например, в листинге 3 область применения директивы import java.math.BigInteger ограничивается исключительно методами объекта App. Если класс java.math.BigInteger понадобится использовать в любом другом классе или объекте внутри пакета mathfun, то его придется импортировать повторно либо вынести директиву import из объекта App на уровень самого пакета. В этом случае все классы из mathfun смогут обращаться к BigInteger по краткому имени.

Листинг 3. Область действия директивы import
package com
{
  package tedneward
  {
    package scala
    {
        // ...
      
      package mathfun
      {
        object App
        {
          import java.math.BigInteger
        
          def factorial(arg : BigInteger) : BigInteger =
          {
            if (arg == BigInteger.ZERO) BigInteger.ONE
            else arg multiply (factorial (arg subtract BigInteger.ONE))
          }
        
          def main(args : Array[String]) : Unit =
          {
            if (args.length > 0)
              System.out.println("factorial " + args(0) +
                " = " + factorial(new BigInteger(args(0))))
            else
              System.out.println("factorial 0 = 1")
          }
        }
      }
    }
  }
}

Возможности импортирования не ограничиваются только импортом классов. В Scala нет различия между типами верхнего уровня и вложенными типами, поэтому при помощи директивы import можно импортировать любой член класса в текущее лексическое пространство имен. В листинге 4 приведен пример импортирования всех членов класса java.math.BigInteger, в результате чего можно опустить полные имена при обращении к константам ZERO и ONE.

Листинг 4. Статическое импортирование без ключевого слова static
package com
{
  package tedneward
  {
    package scala
    {
        // ...
	
      package mathfun
      {
        object App
        {
          import java.math.BigInteger
          import BigInteger._
        
          def factorial(arg : BigInteger) : BigInteger =
          {
            if (arg == ZERO) ONE
            else arg multiply (factorial (arg subtract ONE))
          }
        
          def main(args : Array[String]) : Unit =
          {
            if (args.length > 0)
              System.out.println("factorial " + args(0) +
                " = " + factorial(new BigInteger(args(0))))
            else
              System.out.println("factorial 0 = 1")
          }
        }
      }
    }
  }
}

Символ подчеркивания (помните этот символ-шаблон в Scala?) говорит компилятору о том, что импортироваться должны все члены класса BigInteger. При этом сам класс BigInteger был импортирован предыдущей директивой, поэтому нет необходимости указывать его полное имя. Более того, обе директивы import можно объединить в одну, поскольку ее синтаксис позволяет перечислять импортируемые классы и члены через запятую (листинг 5).

Листинг 5. Несколько директив import в одной строке
package com
{
  package tedneward
  {
    package scala
    {
        // ...
	
      package mathfun
      {
        object App
        {
          import java.math.BigInteger, BigInteger._
        
          def factorial(arg : BigInteger) : BigInteger =
          {
            if (arg == ZERO) ONE
            else arg multiply (factorial (arg subtract ONE))
          }
        
          def main(args : Array[String]) : Unit =
          {
            if (args.length > 0)
              System.out.println("factorial " + args(0) +
                " = " + factorial(new BigInteger(args(0))))
            else
              System.out.println("factorial 0 = 1")
          }
        }
      }
    }
  }
}

Таким образом можно выиграть одну или две строчки кода. Учтите, что нельзя совместить аргументы директивы import, поскольку первый указывает на сам класс BigInteger, а второй – на члены данного класса.

Директиву import можно использовать для импорта не только константных членов класса. Например, рассмотрим библиотеку математических функций, показанную в листинге 6 (полезность данных функций вызывает сомнение, но в данном случае это не важно).

Листинг 6. Библиотека математических функций
package com
{
  package tedneward
  {
    package scala
    {
        // ...
      
      package mathfun
      {
        object BizarroMath
        {
          def bizplus(a : Int, b : Int) = { a - b }
          def bizminus(a : Int, b : Int) = { a + b }
          def bizmultiply(a : Int, b : Int) = { a / b }
          def bizdivide(a : Int, b : Int) = { a * b }
        }
      }
    }
  }
}

Использование этой библиотеки может постепенно превратиться в достаточно утомительное занятие, поскольку придется указывать BizarroMath при каждом обращении к его функциям. Однако Scala позволяет импортировать все члены класса в текущее лексическое пространство, после чего к ним можно будет обращаться как к глобальным функциям (листинг 7).

Листинг 7. Пример использования математических функций
package com
{
  package tedneward
  {
    package scala
    {
      package demonstration
      {
        object App2
        {
          def main(args : Array[String]) : Unit =
          {
            import com.tedneward.scala.mathfun.BizarroMath._
            
            System.out.println("2 + 2 = " + bizplus(2,2))
          }
        }
      }
    }
  }
}

В Scala существуют и другие интересные синтаксические конструкции, позволяющие разработчикам использовать математические функции более естественным образом, например, 2 bizplus 2, но мы пока отложим их рассмотрение. Если вас интересуют подобные возможности, прочитайте о конструкции implicit, которая описывается в книге Одерски, Спуна и Веннерса "Программирование на Scala" (только учтите, что разработчики часто бывают склонны к злоупотреблению такими средствами языка).

Модификаторы доступа

Разбиение на пакеты и импортирование являются неотъемлемой частью механизма инкапсуляции в Scala, однако не меньшую роль в нем, как и в Java, играет возможность выборочного ограничения доступа к конкретным классам и их членам. Подобное разграничение доступа заключается в том, что члены классов помечаются как "открытые" (public), "закрытые" (private) либо имеют некий промежуточный статус.

В Java определены четыре уровня доступа: public, private, protected и уровень пакета (для последнего, к сожалению, не предусмотрено ключевого слова). В Scala же действуют следующие правила:

  • отсутствует понятие уровня доступа по умолчанию (т.е. уровня пакета);
  • по умолчанию используется открытый (public) уровень доступа;
  • закрытый (private) уровень означает, что доступ возможен только из текущей области видимости.

Уровень доступа "protected" (защищенный) в Scala отличается от одноименного уровня в Java. В Scala защищенные члены класса доступы только для дочерних классов, в то время как в Java доступ к ним возможен не только из дочерних классов, но и из всех классов пакета, которому принадлежит данный член. Таким образом, защищенный уровень доступа в Scala накладывает более жесткие ограничения, хотя можно утверждать, что такой его смысл интуитивно понятнее, чем в Java.

Однако ключевым отличием от Java является возможность "квалифицирования" модификаторов доступа при помощи имен пакетов, позволяющая указывать, кроме какого пакета будет действовать заданный уровень доступа. Например, допустим, что доступ к членам объекта BizarroMath должен быть открыт изнутри пакета mathfun, но закрыт для дочерних классов. В этой ситуации можно использовать конструкцию, показанную в листинге 8.

Листинг 8. Библиотека математических функций
package com
{
  package tedneward
  {
    package scala
    {
        // ...
      
      package mathfun
      {
        object BizarroMath
        {
          def bizplus(a : Int, b : Int) = { a - b }
          def bizminus(a : Int, b : Int) = { a + b }
          def bizmultiply(a : Int, b : Int) = { a / b }
          def bizdivide(a : Int, b : Int) = { a * b }
		  
              private[mathfun] def bizexp(a : Int, b: Int) = 0
        }
      }
    }
  }
}

Обратите внимание на модификатор private[mathfun]. Этот модификатор доступа означает, что данный член класса закрыт для классов всех пакетов (в том числе для дочерних классов), кроме mathfun. При этом доступ к bizexp открыт для всех классов из пакета mathfun.

Гибкость таких квалифицированных модификаторов заключается в том, что любой пакет может быть объявлен как закрытый или защищенный от всех пакетов, кроме, например, com (или даже _root_, который является синонимом корневого пакета, поэтому private[_root_] фактически означает то же самое, что public). Таким образом, механизм разграничения доступа обладает значительно большей гибкостью, чем в Java.

В Scala поддерживается еще одна возможность для управления доступом, а именно – объектно-закрытая спецификация, примером которой может служить private[this]. Она означает, что доступ к данному члену класса разрешен только со стороны других членов того же экземпляра класса. При этом доступ со стороны членов других экземпляров закрыт, даже если они относятся к тому же классу. Эта возможность закрывает небольшую брешь в управлении доступом в Java, которая была полезна в основном только в качестве вопросов на собеседовании.

Необходимо учитывать, что модификаторы доступа в Scala должны каким-то образом выражаться в стандартном байт-коде, исполняемом JVM, поэтому некоторые тонкие возможности будут утеряны при компиляции или вызове из приложений Java. Например, если просмотреть скомпилированный код класса BizarroMath с функцией bizexp, помеченной модификатором private[mathfun], при помощи утилиты javap, то вы увидите следующее (листинг 9):

Листинг 9. Представление библиотеки математических функций внутри JVM
Compiled from "packaging.scala"
public final class com.tedneward.scala.mathfun.BizarroMath
   extends java.lang.Object
{
    public static final int $tag();
    public static final int bizexp(int, int);
    public static final int bizdivide(int, int);
    public static final int bizmultiply(int, int);
    public static final int bizminus(int, int);
    public static final int bizplus(int, int);
}

Из второй строки скомпилированного кода следует, что в JVM методу bizexp() был присвоен модификатор доступа public. Это означает, что особенности private[mathfun] будут утеряны сразу после того, как компилятор Scala завершит все проверки доступа. Вследствие этого при написании кода на Scala, который будет использоваться приложениями Java, имеет смысл ограничиваться классическими модификаторами public и private. Даже модификатор protected иногда может трансформироваться в public при компиляции, поэтому при любых сомнениях следует использовать javap, чтобы убедиться в корректности ограничений доступа на уровне байт-кода.

Механизм применения

В предыдущей статье серии под заголовком Коллекции, говоря о массивах в Scala (а именно, о Array[T]) я упомянул, что "получение i-го элемента массива – это еще один пример метода с забавным именем". На самом деле это не совсем так, но тогда мне не хотелось углубляться в такие детали.

Ладно, признаю, что был не прав.

С технической точки зрения, скобки для класса Array[T] представляют собой нечто более сложное, чем просто "метод с забавным именем". В Scala некоторые последовательности символов (например, левая скобка –правая скобка) обладают специальной семантикой, поскольку они часто используются с определенной целью. Выражаясь функциональным языком, в случае скобок такой целью является применение (applying) чего-либо к чему-либо.

Другими словами, в Scala присутствует специальная синтаксическая конструкция (а точнее, специальная синтаксическая связь) для оператора применения "()". Если быть более точным, то компилятор Scala вставляет вызов метода apply() везде, где круглые скобки используются в качестве метода. Например, метод apply() имеет смысл объявить для класса-функтора (т.е. класса, объекты которого должны вести себя как функции), чтобы определить функциональную семантику круглых скобок. Пример приведен в листинге 10.

Листинг 10. Пример использования функтора
class ApplyTest
{
  import org.junit._, Assert._  
  
  @Test def simpleApply =
  {
    class Functor
    {
      def apply() : String =
      {
        "Doing something without arguments"
      }
      
      def apply(i : Int) : String =
      {
        if (i == 0)
          "Done"
        else
          "Applying... " + apply(i - 1)
      }
    }

    val f = new Functor
    assertEquals("Doing something without arguments", f() )
    assertEquals("Applying... Applying... Applying... Done", f(3))
  }
}

Некоторые из вас, возможно, задаются вопросом, чем же функторы отличаются от анонимных функций и замыканий. На самом деле, связь между ними достаточно проста. Тип Function1 в стандартной библиотеке Scala, использующийся для функций, принимающих один аргумент, включает в себя метод apply. Если проанализировать скомпилированный код анонимных классов, которые создаются при компиляции анонимных функций, то можно увидеть, что они все являются потомками Function1 (Function2 или Function3 в зависимости от того, сколько аргументов принимает на вход функция).

Таким образом, в тех случаях, когда анонимные или обычные функции не укладываются в желаемую архитектуру, можно создать класс-функтор. Объекты этого класса можно инициализировать, сохраняя данные в полях, а затем вызывать их при помощи метода (). При этом, в отличие от классического паттерна "стратегия" (Strategy), функторы не должны обязательно быть унаследованы от специального базового класса.

Листинг 11. Еще один пример использования функторов
class ApplyTest
{
  import org.junit._, Assert._  

  // ...
  
  @Test def functorStrategy =
  {
    class GoodAdder
    {
      def apply(lhs : Int, rhs : Int) : Int = lhs + rhs
    }
    class BadAdder(inflateResults : Int)
    {
      def apply(lhs : Int, rhs : Int) : Int = lhs + rhs * inflateResults
    }

    val calculator = new GoodAdder
    assertEquals(4, calculator(2, 2))
    val enronAccountant = new BadAdder(50)
    assertEquals(102, enronAccountant(2, 2))
  }
}

В данном примере можно использовать любой класс, предоставляющий метод apply с нужным количеством аргументов соответствующих типов.

Заключение

Разбиение классов на пакеты, импортирование и модификаторы доступа представляют собой механизмы, обеспечивающие более гибкое управление и инкапсуляцию кода, чем те, которые доступны Java-разработчикам. Например, они позволяют импортировать конкретные методы объекта, чтобы с ними можно было работать как с глобальными функциями, избегая при этом классических неудобств, связанных с последними. При этом использование подобных импортированных методов оказывается необычайно простым, особенно если они реализуют некоторую функциональность высшего порядка. Одним из примеров таких методов может служить функция tryWithLogging, обсуждаемая в статье Не зацикливайтесь!.

Механизм применения в Scala позволяет скрыть детали реализации за функциональным фасадом. Таким образом, программисты могут даже не знать (либо не задумываться) о том, что вызываемая ими сущность является вовсе не функцией, а объектом с некоторой сложной функциональностью. Данный механизм является еще одной гранью Scala как функционального языка программирования. Разумеется, аналогичные механизмы можно реализовать и в других языках, таких как Java, C# или C++, но при этом не удастся добиться той же степени синтаксической чистоты.

На этом пока все, получайте удовольствие от программирования на Scala!


Ресурсы для скачивания


Похожие темы

  • Оригинал статьи: The busy Java developer's guide to Scala: Packages and access modifiers (Тед Ньювард, developerWorks, июль 2008 г.). (EN)
  • Прочитайте статью Функциональное программирование на Java (Абхиджит Белапуркар, Abhijit Belapurkar, developerWorks, июль 2004 г.), в которой обсуждаются преимущества и возможности применения функционального программирования с точки зрения разработчика Java. (EN)
  • Ознакомьтесь со статьей Scala в примерах (Мартин Одерски, Martin Odersky, декабрь 2007 г.), в которой приводится краткое введение в Scala, изобилующее примерами (формат PDF). (EN)
  • Прочитайте книгу Программирование на Scala (Мартин Одерски, Лекс Спун, Lex Spoon и Билл Веннерс, Bill Venners; сигнальный экземпляр вышел в свет в Artima в феврале 2008 г.) – первое подробное введение в Scala, написанное в соавторстве с Биллом Веннерсом. (EN)
  • Ознакомьтесь с разделом часто задаваемых вопросов на сайте Бьёрна Страуструпа- автора и создателя языка C++, который он лично охарактеризовал, как "улучшенный C". (EN)
  • Загрузите Scala и начните ее изучение с этой серии. (EN)

Комментарии

Войдите или зарегистрируйтесь для того чтобы оставлять комментарии или подписаться на них.

static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=513453
ArticleTitle=Путеводитель по Scala для Java-разработчиков: Пакеты и модификаторы доступа
publish-date=08272010